@opengeni/react 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +21 -1
  2. package/dist/artifacts.js +2 -2
  3. package/dist/artifacts.js.map +1 -1
  4. package/dist/{chunk-ERGYJIBB.js → chunk-AWZLIMKX.js} +2 -2
  5. package/dist/{chunk-ERGYJIBB.js.map → chunk-AWZLIMKX.js.map} +1 -1
  6. package/dist/{chunk-FZCP527Y.js → chunk-CJUO7J5Z.js} +2 -2
  7. package/dist/{chunk-742CZ3L3.js → chunk-H5X55XH7.js} +16 -200
  8. package/dist/chunk-H5X55XH7.js.map +1 -0
  9. package/dist/{chunk-PGSX7UCP.js → chunk-LJ2UTJJE.js} +1369 -785
  10. package/dist/chunk-LJ2UTJJE.js.map +1 -0
  11. package/dist/{chunk-CY7WK5FD.js → chunk-LN6TCXD3.js} +22 -1
  12. package/dist/chunk-LN6TCXD3.js.map +1 -0
  13. package/dist/{chunk-TMYGL6X5.js → chunk-MBMPHOMA.js} +71 -6
  14. package/dist/chunk-MBMPHOMA.js.map +1 -0
  15. package/dist/{chunk-RC6BZ44Q.js → chunk-OA6HFRQP.js} +2 -2
  16. package/dist/{chunk-WWEUO2PL.js → chunk-WZMYFJD3.js} +190 -11
  17. package/dist/chunk-WZMYFJD3.js.map +1 -0
  18. package/dist/components/composer-transcription-control.d.ts +3 -0
  19. package/dist/components/open-geni-chat.d.ts +6 -2
  20. package/dist/components/session-conversation.d.ts +25 -19
  21. package/dist/components/session-list.d.ts +4 -1
  22. package/dist/components/tool-action-review.d.ts +29 -2
  23. package/dist/components/tool-review-history.d.ts +8 -4
  24. package/dist/composer.js +1 -1
  25. package/dist/connect.js +1 -1
  26. package/dist/hooks/use-client-config-flags.d.ts +37 -0
  27. package/dist/index.d.ts +3 -3
  28. package/dist/index.js +21 -11
  29. package/dist/index.js.map +1 -1
  30. package/dist/{latest-question-YTOBHCWR.js → latest-question-GPYM2JNU.js} +3 -3
  31. package/dist/realtime/realtime-control.d.ts +2 -0
  32. package/dist/realtime.js +25 -4
  33. package/dist/realtime.js.map +1 -1
  34. package/dist/session-capability-policy.d.ts +20 -1
  35. package/dist/session-ui.js +6 -6
  36. package/dist/session.js +6 -6
  37. package/dist/timeline/index.d.ts +2 -0
  38. package/dist/timeline/retained-loaders.d.ts +27 -0
  39. package/package.json +3 -3
  40. package/src/components/approval-surface.tsx +190 -79
  41. package/src/components/artifacts/session-artifact-viewer.tsx +4 -2
  42. package/src/components/composer-transcription-control.tsx +14 -0
  43. package/src/components/composer.tsx +1 -0
  44. package/src/components/open-geni-chat.tsx +53 -13
  45. package/src/components/session-conversation.tsx +229 -63
  46. package/src/components/session-list.tsx +9 -2
  47. package/src/components/tool-action-review.tsx +502 -239
  48. package/src/components/tool-review-history.tsx +18 -43
  49. package/src/hooks/use-client-config-flags.ts +102 -0
  50. package/src/hooks/use-session-events.ts +19 -16
  51. package/src/hooks/use-transcription.ts +6 -0
  52. package/src/hooks/use-voice-input.ts +6 -0
  53. package/src/index.ts +9 -0
  54. package/src/realtime/realtime-control.tsx +28 -1
  55. package/src/session-capability-policy.ts +119 -6
  56. package/src/timeline/index.ts +10 -0
  57. package/src/timeline/retained-loaders.ts +86 -0
  58. package/src/timeline/tool-display-name.ts +5 -1
  59. package/src/timeline/tool-renderers.tsx +143 -10
  60. package/styles/compiled.css +65 -21
  61. package/styles/tokens.css +1 -1
  62. package/dist/chunk-742CZ3L3.js.map +0 -1
  63. package/dist/chunk-CY7WK5FD.js.map +0 -1
  64. package/dist/chunk-PGSX7UCP.js.map +0 -1
  65. package/dist/chunk-TMYGL6X5.js.map +0 -1
  66. package/dist/chunk-WWEUO2PL.js.map +0 -1
  67. /package/dist/{chunk-FZCP527Y.js.map → chunk-CJUO7J5Z.js.map} +0 -0
  68. /package/dist/{chunk-RC6BZ44Q.js.map → chunk-OA6HFRQP.js.map} +0 -0
  69. /package/dist/{latest-question-YTOBHCWR.js.map → latest-question-GPYM2JNU.js.map} +0 -0
@@ -27,21 +27,21 @@ import {
27
27
  setStartupDetails,
28
28
  useStartupDetails,
29
29
  userMessageLikelyNeedsDisclosure
30
- } from "./chunk-PGSX7UCP.js";
30
+ } from "./chunk-LJ2UTJJE.js";
31
31
  import "./chunk-UK4TINKT.js";
32
32
  import "./chunk-J2QWVSQY.js";
33
33
  import "./chunk-C5X5AJXK.js";
34
34
  import "./chunk-ZYRGEKWS.js";
35
- import "./chunk-FZCP527Y.js";
35
+ import "./chunk-CJUO7J5Z.js";
36
36
  import {
37
37
  ArtifactLabelsProvider,
38
38
  DEFAULT_ARTIFACT_LABELS
39
39
  } from "./chunk-5CVXZA3Y.js";
40
40
  import {
41
41
  createOlderHistoryLoadReceipt
42
- } from "./chunk-WWEUO2PL.js";
43
- import "./chunk-RC6BZ44Q.js";
44
- import "./chunk-ERGYJIBB.js";
42
+ } from "./chunk-WZMYFJD3.js";
43
+ import "./chunk-OA6HFRQP.js";
44
+ import "./chunk-AWZLIMKX.js";
45
45
  import {
46
46
  OpenGeniProvider
47
47
  } from "./chunk-3CTRFMAK.js";
@@ -57,7 +57,7 @@ import {
57
57
  } from "./chunk-XCHWDJ22.js";
58
58
  import "./chunk-A2PI6UMY.js";
59
59
  import "./chunk-JVYQRRC4.js";
60
- import "./chunk-CY7WK5FD.js";
60
+ import "./chunk-LN6TCXD3.js";
61
61
  import "./chunk-RSI7XYXL.js";
62
62
  import "./chunk-BRM6FVZA.js";
63
63
  import "./chunk-LNIRAOA3.js";
package/dist/session.js CHANGED
@@ -1,39 +1,39 @@
1
1
  import {
2
2
  createEmbeddedSessionClient,
3
- isGoalEvent,
4
3
  isLineageRefreshEvent,
5
4
  isSessionMcpApprovalPolicyEvent,
6
- useGoal,
7
5
  useSessionLineage,
8
6
  useSessionMcpApprovalPolicy
9
- } from "./chunk-742CZ3L3.js";
7
+ } from "./chunk-H5X55XH7.js";
10
8
  import {
11
9
  approvalsFromRequiresAction,
12
10
  conversationTimeline,
13
11
  createOlderHistoryLoadReceipt,
14
12
  humanInputRequestFromEvent,
13
+ isGoalEvent,
15
14
  isHumanInputEvent,
16
15
  isTitleEvent,
17
16
  isTurnQueueEvent,
18
17
  projectPendingApprovals,
19
18
  projectPendingHumanInputRequests,
20
19
  useFileAttachments,
20
+ useGoal,
21
21
  useHumanInputRequests,
22
22
  useSession,
23
23
  useSessionControl,
24
24
  useSessionEvents,
25
25
  useTurnQueue
26
- } from "./chunk-WWEUO2PL.js";
26
+ } from "./chunk-WZMYFJD3.js";
27
27
  import {
28
28
  buildTimeline,
29
29
  creditExhaustedFromEvents,
30
30
  extractSessionRef,
31
31
  groupTimeline,
32
32
  sessionStatusFromEvents
33
- } from "./chunk-RC6BZ44Q.js";
33
+ } from "./chunk-OA6HFRQP.js";
34
34
  import {
35
35
  toolDisplayName
36
- } from "./chunk-ERGYJIBB.js";
36
+ } from "./chunk-AWZLIMKX.js";
37
37
  import {
38
38
  FILE_ONLY_MESSAGE_TEXT,
39
39
  composeSendInput,
@@ -2,6 +2,8 @@ export { buildTimeline, creditExhaustedFromEvents, extractSessionRef, groupTimel
2
2
  export type { GroupTimelineOptions } from "./projection.js";
3
3
  export type { ActivityItem, AgentMessageItem, AuthNeededItem, ContextCompactionItem, FleetDecisionItem, FleetDecisionScoreItem, GoalItem, HumanInputAnswerSummary, HumanInputItem, MachineInputBatchItem, MachineInputMember, MemoryItem, KnowledgeItem, NoticeItem, ReasoningItem, SandboxItem, StartupPhase, StartupPhaseItem, SessionStatusItem, TimelineGroup, TimelineItem, TimelineAnnotationSourceDescriptor, TurnEndItem, ToolCallItem, UserMessageItem, WorkerCompletionItem, WorkerItem, } from "./types.js";
4
4
  export { createToolRegistry, rawTypeOf } from "./registry.js";
5
+ export { createSessionRetainedScreenshotLoader, createWorkspaceRetainedArtifactLoader, createWorkspaceRetainedVideoLoader, } from "./retained-loaders.js";
6
+ export type { RetainedArtifactLoaderClient, RetainedScreenshotLoaderClient, RetainedVideoLoaderClient, } from "./retained-loaders.js";
5
7
  export type { CreateToolRegistryOptions, RetainedArtifactLoader, RetainedScreenshotLoader, VideoArtifactPlaybackLoader, ToolRegistry, ToolRegistryEntry, ToolRenderer, ToolRendererProps, } from "./registry.js";
6
8
  export { createDefaultToolRegistry, defaultToolRegistry } from "./tool-renderers.js";
7
9
  export { ActivityRail } from "./activity-rail.js";
@@ -0,0 +1,27 @@
1
+ import type { OpenGeniClient } from "@opengeni/sdk";
2
+ import type { RetainedArtifactLoader, RetainedScreenshotLoader, VideoArtifactPlaybackLoader } from "./registry.js";
3
+ /** SDK methods the workspace retained-artifact loader needs. */
4
+ export type RetainedArtifactLoaderClient = Pick<OpenGeniClient, "createRetainedArtifactDownloadUrl" | "downloadRetainedArtifact">;
5
+ /** SDK method the session retained-screenshot loader needs. */
6
+ export type RetainedScreenshotLoaderClient = Pick<OpenGeniClient, "downloadRetainedScreenshot">;
7
+ /** SDK method the generated-video playback loader needs. */
8
+ export type RetainedVideoLoaderClient = Pick<OpenGeniClient, "createVideoArtifactPlaybackSource">;
9
+ /**
10
+ * Bind permanent retained-artifact retrieval (generated images, published
11
+ * files) to one authenticated workspace, for `MessageTimeline`'s
12
+ * `loadRetainedArtifact`. Images get a short-lived URL; files are assembled
13
+ * from verified byte ranges.
14
+ */
15
+ export declare function createWorkspaceRetainedArtifactLoader(client: RetainedArtifactLoaderClient, workspaceId: string): RetainedArtifactLoader;
16
+ /**
17
+ * Bind the authenticated screenshot downloader to one workspace and session,
18
+ * for `MessageTimeline`'s `loadRetainedScreenshot`. Fetched metadata must match
19
+ * the timeline receipt before bytes are handed to React; errors deliberately
20
+ * contain no storage locator or content.
21
+ */
22
+ export declare function createSessionRetainedScreenshotLoader(client: RetainedScreenshotLoaderClient, workspaceId: string, sessionId: string): RetainedScreenshotLoader;
23
+ /**
24
+ * Bind expiring generated-video playback sources to one authenticated
25
+ * workspace, for `MessageTimeline`'s `loadVideoArtifactPlayback`.
26
+ */
27
+ export declare function createWorkspaceRetainedVideoLoader(client: RetainedVideoLoaderClient, workspaceId: string): VideoArtifactPlaybackLoader;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/react",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "React hooks and styled components for OpenGeni: live session streaming, chat composer, message timeline, session status, and fleet views — token-themed (CSS variables), dark-first, built on Tailwind v4 + Radix + Motion.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -153,8 +153,8 @@
153
153
  "@dnd-kit/utilities": "3.2.2",
154
154
  "@fontsource-variable/noto-sans-arabic": "5.2.10",
155
155
  "@fontsource-variable/noto-sans-jp": "5.2.10",
156
- "@opengeni/connect": "1.3.0",
157
- "@opengeni/sdk": "1.3.0",
156
+ "@opengeni/connect": "1.4.0",
157
+ "@opengeni/sdk": "1.4.0",
158
158
  "clsx": "^2.1.1",
159
159
  "dequal": "2.0.3",
160
160
  "lucide-react": "^1.8.0",
@@ -1,4 +1,6 @@
1
- import { useEffect, useRef, useState, type ReactNode } from "react";
1
+ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
2
+ import { dequal } from "dequal";
3
+ import { ChevronLeftIcon, ChevronRightIcon, ShieldCheckIcon } from "lucide-react";
2
4
  import {
3
5
  OpenGeniApiError,
4
6
  toolReviewAction,
@@ -13,6 +15,9 @@ import { toolDisplayName } from "../timeline/tool-display-name";
13
15
  import {
14
16
  ToolActionReviewCard,
15
17
  ToolActionReviewDetails,
18
+ ToolReviewFrame,
19
+ DeclineButton,
20
+ UNRECOVERABLE_TEXT,
16
21
  type ToolReviewDetailsLoader,
17
22
  } from "./tool-action-review";
18
23
 
@@ -37,10 +42,15 @@ export const defaultApprovalSurfaceMessages: ApprovalSurfaceMessages = {
37
42
  formatToolName: (name) =>
38
43
  name.includes("__") || /^[a-f0-9]{64}$/.test(name)
39
44
  ? toolDisplayName(name)
40
- : name.replaceAll("_", " ").replaceAll(".", " › "),
45
+ : humanToolName(name.replaceAll("_", " ").replaceAll(".", " › ")),
41
46
  showDetails: "View full details",
42
47
  hideDetails: "Back to review",
43
48
  };
49
+ /** "batch modify messages" → "Modify messages": how a call is grouped is not what it does. */
50
+ function humanToolName(name: string): string {
51
+ const phrase = name.replace(/^batch\s+/i, "").trim();
52
+ return phrase ? phrase.charAt(0).toUpperCase() + phrase.slice(1) : name;
53
+ }
44
54
  export type ApprovalSurfaceProps = {
45
55
  approvals: PendingApproval[];
46
56
  onApprove: (approval: PendingApproval) => void | Promise<void>;
@@ -88,9 +98,10 @@ export function ApprovalSurface({
88
98
  }: ApprovalSurfaceProps) {
89
99
  const messages = { ...defaultApprovalSurfaceMessages, ...overrides };
90
100
  const formatError = useErrorMessage();
91
- const [pending, setPending] = useState<{ id: string; decision: "approve" | "reject" } | null>(
92
- null,
93
- );
101
+ const [pending, setPending] = useState<{
102
+ id: string;
103
+ decision: "approve" | "reject";
104
+ } | null>(null);
94
105
  const pendingRef = useRef<string | null>(null);
95
106
  const [decisionError, setDecisionError] = useState<{ cause: unknown } | null>(null);
96
107
  const [localSelectedId, setLocalSelectedId] = useState<string | null>(null);
@@ -137,39 +148,43 @@ export function ApprovalSurface({
137
148
  <section
138
149
  ref={surface}
139
150
  aria-label={messages.title}
140
- className={cn("og-root box-border w-full min-w-0 border-t border-og-border", className)}
151
+ className={cn("og-root box-border w-full min-w-0 space-y-2 py-2", className)}
141
152
  data-og-approval-surface=""
142
153
  >
143
154
  {approvals.length > 1 && (
144
155
  <nav
145
156
  aria-label="Actions to review"
146
- className="flex flex-wrap items-center justify-between gap-2 pt-3 text-og-xs text-og-fg-muted"
157
+ className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1 text-og-xs text-og-fg-muted"
147
158
  >
148
- <span>{approvals.length} actions need review</span>
149
- <div className="flex items-center gap-3">
159
+ <span className="font-medium text-og-status-waiting">
160
+ {approvals.length} actions are waiting for you
161
+ </span>
162
+ <div className="flex items-center gap-1">
150
163
  <button
151
164
  type="button"
165
+ aria-label="Previous action"
152
166
  disabled={selectedIndex === 0}
153
167
  onClick={() => setSelectedId(approvals[selectedIndex - 1]!.id)}
154
- className="min-h-11 disabled:opacity-40"
168
+ className="inline-flex min-h-8 min-w-8 items-center justify-center rounded-og-md hover:bg-og-hover hover:text-og-fg disabled:opacity-40 disabled:hover:bg-transparent [@media(pointer:coarse)]:min-h-11 [@media(pointer:coarse)]:min-w-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
155
169
  >
156
- Previous
170
+ <ChevronLeftIcon className="size-4" aria-hidden="true" />
157
171
  </button>
158
- <span>
172
+ <span className="tabular-nums" aria-live="polite">
159
173
  {selectedIndex + 1} of {approvals.length}
160
174
  </span>
161
175
  <button
162
176
  type="button"
177
+ aria-label="Next action"
163
178
  disabled={selectedIndex === approvals.length - 1}
164
179
  onClick={() => setSelectedId(approvals[selectedIndex + 1]!.id)}
165
- className="min-h-11 disabled:opacity-40"
180
+ className="inline-flex min-h-8 min-w-8 items-center justify-center rounded-og-md hover:bg-og-hover hover:text-og-fg disabled:opacity-40 disabled:hover:bg-transparent [@media(pointer:coarse)]:min-h-11 [@media(pointer:coarse)]:min-w-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
166
181
  >
167
- Next
182
+ <ChevronRightIcon className="size-4" aria-hidden="true" />
168
183
  </button>
169
184
  </div>
170
185
  </nav>
171
186
  )}
172
- <div className="divide-y divide-og-border">
187
+ <div>
173
188
  {[selected].map((approval) => (
174
189
  <ApprovalRequest
175
190
  key={approval.id}
@@ -187,7 +202,7 @@ export function ApprovalSurface({
187
202
  ))}
188
203
  </div>
189
204
  {errorText && (
190
- <p role="alert" className="m-0 pb-3 text-og-sm text-og-status-failed">
205
+ <p role="alert" className="m-0 px-1 text-og-sm text-og-status-failed">
191
206
  {errorText}
192
207
  </p>
193
208
  )}
@@ -215,96 +230,197 @@ function ApprovalRequest({
215
230
  onApprove: () => void;
216
231
  onReject: () => void;
217
232
  }) {
218
- const [loaded, setLoaded] = useState<ToolActionReview | null>(null);
219
- const [failed, setFailed] = useState(false);
220
- const [legacy, setLegacy] = useState(false);
221
233
  const [retry, setRetry] = useState(0);
234
+ // Event projection recreates the same approval on unrelated timeline updates.
235
+ // Keep one request for equal facts, but never reuse it across changed facts,
236
+ // an explicit retry, or a loader change (the host's client/access boundary).
237
+ const requestRef = useRef({ approval, loadReview, retry });
238
+ if (
239
+ requestRef.current.loadReview !== loadReview ||
240
+ requestRef.current.retry !== retry ||
241
+ !dequal(requestRef.current.approval, approval)
242
+ ) {
243
+ requestRef.current = { approval, loadReview, retry };
244
+ }
245
+ const request = requestRef.current;
246
+ const [result, setResult] = useState<{
247
+ request: typeof request;
248
+ review?: ToolActionReview;
249
+ failed?: boolean;
250
+ legacy?: boolean;
251
+ denied?: boolean;
252
+ } | null>(null);
253
+ // Hide stale facts during render, before passive effect cleanup can run.
254
+ const current = result?.request === request ? result : null;
255
+ const loaded = current?.review;
256
+ const failed = current?.failed;
257
+ const legacy = current?.legacy;
258
+ const denied = current?.denied;
222
259
  const [detailPath, setDetailPath] = useState<string | null>(null);
223
260
  useEffect(() => {
224
261
  let active = true;
225
- setLoaded(null);
226
- setFailed(false);
227
- setLegacy(false);
228
- if (loadReview)
229
- void loadReview(approval).then(
262
+ if (request.loadReview)
263
+ void request.loadReview(request.approval).then(
230
264
  (review) => {
231
265
  if (active) {
232
- if (review.id !== approval.id) setFailed(true);
233
- else setLoaded(review);
266
+ setResult(
267
+ review.id !== request.approval.id ? { request, failed: true } : { request, review },
268
+ );
234
269
  }
235
270
  },
236
271
  (error) => {
237
272
  if (active) {
238
- if (
273
+ // 404: no saved review (older or non-connector actions); use the
274
+ // approval's own arguments. 401/403: this person cannot review it.
275
+ if (error instanceof OpenGeniApiError && error.status === 404)
276
+ setResult({ request, legacy: true });
277
+ else if (
239
278
  error instanceof OpenGeniApiError &&
240
- error.status === 404 &&
241
- approval.arguments !== undefined
279
+ (error.status === 401 || error.status === 403)
242
280
  )
243
- setLegacy(true);
244
- else setFailed(true);
281
+ setResult({ request, failed: true, denied: true });
282
+ else setResult({ request, failed: true });
245
283
  }
246
284
  },
247
285
  );
248
286
  return () => {
249
287
  active = false;
250
288
  };
251
- }, [approval, retry, loadReview]);
289
+ }, [request]);
290
+ const stableApproval = request.approval;
291
+ const { formatToolName, approve: approveLabel } = messages;
292
+ const fallback = useMemo<ToolActionReview>(
293
+ () => ({
294
+ version: 1,
295
+ id: stableApproval.id,
296
+ actionDigest: "",
297
+ revision: "legacy",
298
+ status: "pending",
299
+ ...toolReviewAction(stableApproval.name, stableApproval.arguments, {
300
+ kind: "generic",
301
+ title:
302
+ stableApproval.display?.title ??
303
+ formatToolName(stableApproval.display?.toolName ?? stableApproval.name),
304
+ }),
305
+ ...(stableApproval.display?.accountLabel
306
+ ? { accountLabel: stableApproval.display.accountLabel }
307
+ : {}),
308
+ ...toolReviewFields(stableApproval.arguments),
309
+ reason: "",
310
+ createdAt: "",
311
+ updatedAt: "",
312
+ approveLabel,
313
+ availableActions: stableApproval.arguments === undefined ? [] : ["approve", "reject"],
314
+ detailsAvailable: stableApproval.arguments !== undefined,
315
+ }),
316
+ [stableApproval, formatToolName, approveLabel],
317
+ );
318
+ const loadLegacyDetails = useCallback<ToolReviewDetailsLoader>(
319
+ async (value, path, offset) => ({
320
+ version: 1,
321
+ id: value.id,
322
+ actionDigest: value.actionDigest,
323
+ ...toolReviewDetails(stableApproval.arguments, undefined, path, offset),
324
+ }),
325
+ [stableApproval],
326
+ );
252
327
  if (loadReview && !loaded && !failed && !legacy)
253
328
  return (
254
- <div className="py-5 text-og-sm text-og-fg-muted" role="status">
255
- Loading action details…
256
- </div>
329
+ <ToolReviewFrame
330
+ tone="waiting"
331
+ icon={<ShieldCheckIcon className="size-4" aria-hidden="true" />}
332
+ eyebrow={messages.title}
333
+ title={fallback.title}
334
+ subtitle={fallback.accountLabel}
335
+ aria-busy
336
+ data-og-tool-review-loading=""
337
+ >
338
+ <div role="status" className="space-y-2">
339
+ <span className="sr-only">Loading action details…</span>
340
+ <div className="h-3 w-3/4 animate-pulse rounded-og-sm bg-og-fg/8" aria-hidden="true" />
341
+ <div className="h-3 w-1/2 animate-pulse rounded-og-sm bg-og-fg/8" aria-hidden="true" />
342
+ </div>
343
+ </ToolReviewFrame>
344
+ );
345
+ if (denied)
346
+ return (
347
+ <ToolReviewFrame
348
+ tone="waiting"
349
+ icon={<ShieldCheckIcon className="size-4" aria-hidden="true" />}
350
+ eyebrow={messages.title}
351
+ title={fallback.title}
352
+ subtitle={fallback.accountLabel}
353
+ >
354
+ <p role="alert" className="m-0 text-og-sm text-og-fg">
355
+ You don't have access to review this action. Ask someone with access to this chat to
356
+ decide.
357
+ </p>
358
+ </ToolReviewFrame>
257
359
  );
258
360
  if (failed)
259
361
  return (
260
- <div className="py-4 text-og-sm text-og-fg" role="alert">
261
- Action details could not be loaded.{" "}
262
- <button
263
- type="button"
264
- className="min-h-11 underline underline-offset-4"
265
- onClick={() => setRetry((value) => value + 1)}
362
+ <ToolReviewFrame
363
+ tone="waiting"
364
+ icon={<ShieldCheckIcon className="size-4" aria-hidden="true" />}
365
+ eyebrow={messages.title}
366
+ title={fallback.title}
367
+ subtitle={fallback.accountLabel}
368
+ footer={
369
+ <div className="ml-auto">
370
+ <DeclineButton
371
+ busy={Boolean(actions.disabled || actions.submitting)}
372
+ submitting={actions.submitting === "reject"}
373
+ label={messages.reject}
374
+ onClick={actions.onReject}
375
+ />
376
+ </div>
377
+ }
378
+ >
379
+ <p role="alert" className="m-0 text-og-sm text-og-fg">
380
+ Couldn't load this action, so it can't be approved yet. You can still decline it.{" "}
381
+ <button
382
+ type="button"
383
+ className="min-h-8 font-medium underline underline-offset-4 [@media(pointer:coarse)]:min-h-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
384
+ onClick={() => setRetry((value) => value + 1)}
385
+ >
386
+ Try again
387
+ </button>
388
+ </p>
389
+ </ToolReviewFrame>
390
+ );
391
+ if (!loaded && stableApproval.arguments === undefined)
392
+ return (
393
+ <div data-approval-id={approval.id} data-review-origin="pending">
394
+ {custom}
395
+ <ToolReviewFrame
396
+ tone="waiting"
397
+ icon={<ShieldCheckIcon className="size-4" aria-hidden="true" />}
398
+ eyebrow={messages.title}
399
+ title={fallback.title}
400
+ subtitle={fallback.accountLabel}
401
+ data-og-tool-review=""
402
+ footer={
403
+ <div className="ml-auto">
404
+ <DeclineButton
405
+ busy={Boolean(actions.disabled || actions.submitting)}
406
+ submitting={actions.submitting === "reject"}
407
+ label={messages.reject}
408
+ onClick={actions.onReject}
409
+ />
410
+ </div>
411
+ }
266
412
  >
267
- Try again
268
- </button>
413
+ <p className="m-0 text-og-sm text-og-fg">{UNRECOVERABLE_TEXT}</p>
414
+ </ToolReviewFrame>
269
415
  </div>
270
416
  );
271
- const fallback: ToolActionReview = {
272
- version: 1,
273
- id: approval.id,
274
- actionDigest: "",
275
- revision: "legacy",
276
- status: "pending",
277
- ...toolReviewAction(approval.name, approval.arguments, {
278
- kind: "generic",
279
- title:
280
- approval.display?.title ??
281
- messages.formatToolName(approval.display?.toolName ?? approval.name),
282
- }),
283
- ...(approval.display?.accountLabel ? { accountLabel: approval.display.accountLabel } : {}),
284
- ...toolReviewFields(approval.arguments),
285
- reason: "This action requires your approval.",
286
- createdAt: "",
287
- updatedAt: "",
288
- approveLabel: messages.approve,
289
- availableActions: approval.arguments === undefined ? [] : ["approve", "reject"],
290
- detailsAvailable: approval.arguments !== undefined,
291
- };
292
417
  const review = loaded ?? fallback;
293
418
  if (detailPath !== null)
294
419
  return (
295
420
  <ToolActionReviewDetails
296
421
  review={review}
297
422
  path={detailPath}
298
- load={
299
- loaded && loadDetails
300
- ? loadDetails
301
- : async (value, path, offset) => ({
302
- version: 1,
303
- id: value.id,
304
- actionDigest: value.actionDigest,
305
- ...toolReviewDetails(approval.arguments, undefined, path, offset),
306
- })
307
- }
423
+ load={loaded && loadDetails ? loadDetails : loadLegacyDetails}
308
424
  onBack={() => {
309
425
  const path = detailPath;
310
426
  setDetailPath(null);
@@ -340,11 +456,6 @@ function ApprovalRequest({
340
456
  : undefined
341
457
  }
342
458
  />
343
- {!loaded && approval.arguments === undefined && (
344
- <p className="pb-3 text-og-sm text-og-fg-muted">
345
- Open the task with a compatible client to review this action.
346
- </p>
347
- )}
348
459
  </div>
349
460
  );
350
461
  }
@@ -203,7 +203,7 @@ function EditableBody({
203
203
  workspaceId: string;
204
204
  sessionId: string;
205
205
  result:
206
- | { kind: "ready"; config: NonNullable<ClientConfig["artifacts"]> }
206
+ | { kind: "ready"; config: Exclude<NonNullable<ClientConfig["artifacts"]>, false> }
207
207
  | { kind: "unavailable" }
208
208
  | { kind: "error"; error: unknown };
209
209
  } | null>(null);
@@ -359,7 +359,9 @@ function EditableBody({
359
359
  );
360
360
  }
361
361
 
362
- function artifactCapabilityKey(config: NonNullable<ClientConfig["artifacts"]>): string {
362
+ function artifactCapabilityKey(
363
+ config: Exclude<NonNullable<ClientConfig["artifacts"]>, false>,
364
+ ): string {
363
365
  return JSON.stringify([
364
366
  config.editableLiveUrl,
365
367
  config.cachePartition.accountId,
@@ -50,6 +50,9 @@ export type ComposerTranscriptionMessages = {
50
50
  errorPermissionDenied: string;
51
51
  errorNotSupported: string;
52
52
  errorUnavailable: string;
53
+ errorInsufficientCredits: string;
54
+ errorAllowanceExhausted: string;
55
+ errorPolicyBlocked: string;
53
56
  errorTooLarge: string;
54
57
  errorInvalidAudio: string;
55
58
  errorStorageUnavailable: string;
@@ -78,6 +81,10 @@ const defaultMessages: ComposerTranscriptionMessages = {
78
81
  errorPermissionDenied: "Microphone permission was denied. Your draft was not changed.",
79
82
  errorNotSupported: "Voice input is not supported on this device.",
80
83
  errorUnavailable: "Voice input is not configured.",
84
+ errorInsufficientCredits:
85
+ "Voice input needs Opengeni credits. Add credits, then retry your saved recording.",
86
+ errorAllowanceExhausted: "Your usage limit was reached. Your recording is saved for later.",
87
+ errorPolicyBlocked: "Voice input isn't allowed for this account or workspace.",
81
88
  errorTooLarge: "Recording is too large. Try a shorter message.",
82
89
  errorInvalidAudio: "The recording could not be read. Try again.",
83
90
  errorStorageUnavailable: "Voice input stopped because audio could not be saved safely.",
@@ -518,6 +525,13 @@ function restingLevels(count: number): number[] {
518
525
 
519
526
  function transcriptionErrorMessage(code: string, messages: ComposerTranscriptionMessages): string {
520
527
  switch (code) {
528
+ case "insufficient_credits":
529
+ return messages.errorInsufficientCredits;
530
+ case "allowance_exhausted":
531
+ case "monthly_model_cost_limit":
532
+ return messages.errorAllowanceExhausted;
533
+ case "policy_blocked":
534
+ return messages.errorPolicyBlocked;
521
535
  case "permission_denied":
522
536
  return messages.errorPermissionDenied;
523
537
  case "not_supported":
@@ -967,6 +967,7 @@ export const Surface = forwardRef<HTMLDivElement, ComposerSurfaceProps>(function
967
967
  const controller = useComposerController();
968
968
  return (
969
969
  <div
970
+ data-og-composer-surface=""
970
971
  {...props}
971
972
  ref={ref}
972
973
  onDragOver={controller.attachments ? controller.handleDragOver : undefined}