@opengeni/react 1.2.1-canary.37222965366001 → 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.
- package/README.md +52 -1
- package/dist/artifacts.js +2 -2
- package/dist/artifacts.js.map +1 -1
- package/dist/{chunk-ERGYJIBB.js → chunk-AWZLIMKX.js} +2 -2
- package/dist/{chunk-ERGYJIBB.js.map → chunk-AWZLIMKX.js.map} +1 -1
- package/dist/{chunk-FZCP527Y.js → chunk-CJUO7J5Z.js} +2 -2
- package/dist/{chunk-742CZ3L3.js → chunk-H5X55XH7.js} +16 -200
- package/dist/chunk-H5X55XH7.js.map +1 -0
- package/dist/{chunk-4OTO7KBK.js → chunk-LJ2UTJJE.js} +2737 -1580
- package/dist/chunk-LJ2UTJJE.js.map +1 -0
- package/dist/{chunk-CY7WK5FD.js → chunk-LN6TCXD3.js} +22 -1
- package/dist/chunk-LN6TCXD3.js.map +1 -0
- package/dist/{chunk-TMYGL6X5.js → chunk-MBMPHOMA.js} +71 -6
- package/dist/chunk-MBMPHOMA.js.map +1 -0
- package/dist/{chunk-RC6BZ44Q.js → chunk-OA6HFRQP.js} +2 -2
- package/dist/{chunk-X34BSY2R.js → chunk-WZMYFJD3.js} +242 -63
- package/dist/chunk-WZMYFJD3.js.map +1 -0
- package/dist/components/approval-surface.d.ts +11 -13
- package/dist/components/composer-transcription-control.d.ts +3 -0
- package/dist/components/open-geni-chat.d.ts +26 -7
- package/dist/components/session-conversation.d.ts +26 -3
- package/dist/components/session-list.d.ts +4 -1
- package/dist/components/session-proxy-scope.d.ts +16 -2
- package/dist/components/tool-action-review.d.ts +47 -0
- package/dist/components/tool-review-history.d.ts +25 -0
- package/dist/composer.js +1 -1
- package/dist/connect.js +1 -1
- package/dist/hooks/use-client-config-flags.d.ts +37 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +27 -11
- package/dist/index.js.map +1 -1
- package/dist/{latest-question-YTOBHCWR.js → latest-question-GPYM2JNU.js} +3 -3
- package/dist/realtime/realtime-control.d.ts +2 -0
- package/dist/realtime.js +25 -4
- package/dist/realtime.js.map +1 -1
- package/dist/session-capability-policy.d.ts +20 -1
- package/dist/session-ui.d.ts +4 -1
- package/dist/session-ui.js +12 -6
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +6 -6
- package/dist/timeline/index.d.ts +2 -0
- package/dist/timeline/retained-loaders.d.ts +27 -0
- package/package.json +3 -3
- package/src/components/approval-surface.tsx +368 -239
- package/src/components/artifacts/session-artifact-viewer.tsx +4 -2
- package/src/components/composer-transcription-control.tsx +14 -0
- package/src/components/composer.tsx +1 -0
- package/src/components/open-geni-chat.tsx +245 -72
- package/src/components/session-conversation.tsx +243 -58
- package/src/components/session-list.tsx +9 -2
- package/src/components/session-proxy-scope.tsx +47 -7
- package/src/components/tool-action-review.tsx +649 -0
- package/src/components/tool-review-history.tsx +88 -0
- package/src/hooks/use-client-config-flags.ts +102 -0
- package/src/hooks/use-session-events.ts +19 -16
- package/src/hooks/use-transcription.ts +6 -0
- package/src/hooks/use-voice-input.ts +6 -0
- package/src/index.ts +18 -1
- package/src/realtime/realtime-control.tsx +28 -1
- package/src/session-capability-policy.ts +119 -6
- package/src/session-ui.ts +9 -1
- package/src/timeline/index.ts +10 -0
- package/src/timeline/retained-loaders.ts +86 -0
- package/src/timeline/tool-display-name.ts +5 -1
- package/src/timeline/tool-renderers.tsx +148 -8
- package/styles/compiled.css +79 -15
- package/styles/tokens.css +1 -1
- package/dist/chunk-4OTO7KBK.js.map +0 -1
- package/dist/chunk-742CZ3L3.js.map +0 -1
- package/dist/chunk-CY7WK5FD.js.map +0 -1
- package/dist/chunk-TMYGL6X5.js.map +0 -1
- package/dist/chunk-X34BSY2R.js.map +0 -1
- /package/dist/{chunk-FZCP527Y.js.map → chunk-CJUO7J5Z.js.map} +0 -0
- /package/dist/{chunk-RC6BZ44Q.js.map → chunk-OA6HFRQP.js.map} +0 -0
- /package/dist/{latest-question-YTOBHCWR.js.map → latest-question-GPYM2JNU.js.map} +0 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArrowLeftIcon,
|
|
3
|
+
CheckIcon,
|
|
4
|
+
ChevronRightIcon,
|
|
5
|
+
CircleAlertIcon,
|
|
6
|
+
CircleSlashIcon,
|
|
7
|
+
LoaderCircleIcon,
|
|
8
|
+
ShieldCheckIcon,
|
|
9
|
+
} from "lucide-react";
|
|
10
|
+
import { useEffect, useId, useRef, useState, type HTMLAttributes, type ReactNode } from "react";
|
|
11
|
+
import type { ToolActionReview, ToolReviewDetailsPage, ToolReviewStatus } from "@opengeni/sdk";
|
|
12
|
+
import { cn } from "../lib/cn";
|
|
13
|
+
|
|
14
|
+
export type ToolReviewDetailsLoader = (
|
|
15
|
+
review: ToolActionReview,
|
|
16
|
+
path: string,
|
|
17
|
+
offset: number,
|
|
18
|
+
) => Promise<ToolReviewDetailsPage>;
|
|
19
|
+
|
|
20
|
+
const statusText: Record<ToolReviewStatus, string> = {
|
|
21
|
+
pending: "Approval needed",
|
|
22
|
+
approved: "Approved",
|
|
23
|
+
executing: "Approved · running",
|
|
24
|
+
completed: "Approved · done",
|
|
25
|
+
partial: "Partly done",
|
|
26
|
+
unknown: "Outcome unknown",
|
|
27
|
+
rejected: "Declined",
|
|
28
|
+
cancelled: "Cancelled",
|
|
29
|
+
expired: "Expired",
|
|
30
|
+
revoked: "Access unavailable",
|
|
31
|
+
blocked: "Blocked",
|
|
32
|
+
stale: "No longer valid",
|
|
33
|
+
failed: "Failed",
|
|
34
|
+
unavailable: "Details unavailable",
|
|
35
|
+
};
|
|
36
|
+
const statusDetail: Partial<Record<ToolReviewStatus, string>> = {
|
|
37
|
+
unknown: "It may have run. Check the result before trying again.",
|
|
38
|
+
rejected: "This action did not run.",
|
|
39
|
+
cancelled: "This action did not run.",
|
|
40
|
+
expired: "This action did not run. Ask the agent to prepare it again.",
|
|
41
|
+
revoked: "This action did not run. Reconnect the account, then ask the agent to try again.",
|
|
42
|
+
stale: "The tool or account changed after this was prepared, so it did not run.",
|
|
43
|
+
partial: "Some of it ran. Check the result before continuing.",
|
|
44
|
+
failed: "The action ran but returned an error.",
|
|
45
|
+
};
|
|
46
|
+
type Tone = "waiting" | "ok" | "failed" | "muted";
|
|
47
|
+
const statusTone: Record<ToolReviewStatus, Tone> = {
|
|
48
|
+
pending: "waiting",
|
|
49
|
+
approved: "ok",
|
|
50
|
+
executing: "ok",
|
|
51
|
+
completed: "ok",
|
|
52
|
+
partial: "failed",
|
|
53
|
+
unknown: "failed",
|
|
54
|
+
rejected: "muted",
|
|
55
|
+
cancelled: "muted",
|
|
56
|
+
expired: "muted",
|
|
57
|
+
revoked: "muted",
|
|
58
|
+
blocked: "muted",
|
|
59
|
+
stale: "muted",
|
|
60
|
+
failed: "failed",
|
|
61
|
+
unavailable: "muted",
|
|
62
|
+
};
|
|
63
|
+
const toneText: Record<Tone, string> = {
|
|
64
|
+
waiting: "text-og-status-waiting",
|
|
65
|
+
ok: "text-og-fg-muted",
|
|
66
|
+
failed: "text-og-status-failed",
|
|
67
|
+
muted: "text-og-fg-muted",
|
|
68
|
+
};
|
|
69
|
+
/** Fields that only identify the selection; the count, examples and details cover them. */
|
|
70
|
+
const selectionPaths = new Set([
|
|
71
|
+
"/messageIds",
|
|
72
|
+
"/messageId",
|
|
73
|
+
"/threadId",
|
|
74
|
+
"/addLabelIds",
|
|
75
|
+
"/removeLabelIds",
|
|
76
|
+
"/labelIds",
|
|
77
|
+
]);
|
|
78
|
+
/** Shown when an older request's saved values are gone: approval is impossible, decline is not. */
|
|
79
|
+
export const UNRECOVERABLE_TEXT =
|
|
80
|
+
"The details of this older request couldn't be recovered, so it can't be approved. Decline it to let the agent continue.";
|
|
81
|
+
/** Long argument lists stay readable; the full set is one click away in details. */
|
|
82
|
+
const VISIBLE_FIELDS = 6;
|
|
83
|
+
|
|
84
|
+
function StatusIcon({ status, busy }: { status: ToolReviewStatus; busy?: boolean }) {
|
|
85
|
+
const className = "size-4";
|
|
86
|
+
if (busy || status === "executing" || status === "approved")
|
|
87
|
+
return <LoaderCircleIcon aria-hidden="true" className={cn(className, "animate-spin")} />;
|
|
88
|
+
if (status === "pending") return <ShieldCheckIcon aria-hidden="true" className={className} />;
|
|
89
|
+
if (status === "completed") return <CheckIcon aria-hidden="true" className={className} />;
|
|
90
|
+
if (statusTone[status] === "failed")
|
|
91
|
+
return <CircleAlertIcon aria-hidden="true" className={className} />;
|
|
92
|
+
return <CircleSlashIcon aria-hidden="true" className={className} />;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Card chrome shared by the review, its loading state and its load failure. */
|
|
96
|
+
export function ToolReviewFrame({
|
|
97
|
+
tone,
|
|
98
|
+
icon,
|
|
99
|
+
eyebrow,
|
|
100
|
+
title,
|
|
101
|
+
titleId,
|
|
102
|
+
subtitle,
|
|
103
|
+
children,
|
|
104
|
+
footer,
|
|
105
|
+
bare = false,
|
|
106
|
+
...rest
|
|
107
|
+
}: {
|
|
108
|
+
tone: Tone;
|
|
109
|
+
icon: ReactNode;
|
|
110
|
+
eyebrow: ReactNode;
|
|
111
|
+
title: ReactNode;
|
|
112
|
+
titleId?: string | undefined;
|
|
113
|
+
subtitle?: ReactNode;
|
|
114
|
+
children?: ReactNode;
|
|
115
|
+
footer?: ReactNode;
|
|
116
|
+
bare?: boolean | undefined;
|
|
117
|
+
} & Omit<HTMLAttributes<HTMLElement>, "title">) {
|
|
118
|
+
const waiting = tone === "waiting";
|
|
119
|
+
return (
|
|
120
|
+
<article
|
|
121
|
+
aria-labelledby={titleId}
|
|
122
|
+
{...rest}
|
|
123
|
+
className={cn(
|
|
124
|
+
"min-w-0 text-og-fg",
|
|
125
|
+
!bare &&
|
|
126
|
+
(waiting
|
|
127
|
+
? "overflow-hidden rounded-og-lg border border-og-status-waiting/35 bg-og-status-waiting/5 shadow-og-sm"
|
|
128
|
+
: "overflow-hidden rounded-og-lg border border-og-border bg-og-surface-1"),
|
|
129
|
+
rest.className,
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
<header className={cn("flex min-w-0 items-start gap-3", bare ? "pb-1" : "px-4 py-3")}>
|
|
133
|
+
<span
|
|
134
|
+
className={cn(
|
|
135
|
+
"mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-og-md",
|
|
136
|
+
waiting
|
|
137
|
+
? "bg-og-status-waiting/12 text-og-status-waiting"
|
|
138
|
+
: tone === "failed"
|
|
139
|
+
? "bg-og-status-failed/10 text-og-status-failed"
|
|
140
|
+
: "bg-og-fg/5 text-og-fg-muted",
|
|
141
|
+
)}
|
|
142
|
+
>
|
|
143
|
+
{icon}
|
|
144
|
+
</span>
|
|
145
|
+
<div className="min-w-0 flex-1">
|
|
146
|
+
<p
|
|
147
|
+
className={cn("m-0 text-og-xs font-medium", toneText[tone])}
|
|
148
|
+
role="status"
|
|
149
|
+
aria-live="polite"
|
|
150
|
+
>
|
|
151
|
+
{eyebrow}
|
|
152
|
+
</p>
|
|
153
|
+
<h3
|
|
154
|
+
tabIndex={-1}
|
|
155
|
+
id={titleId}
|
|
156
|
+
className="m-0 mt-0.5 break-words text-og-md font-semibold leading-snug text-og-fg outline-hidden [overflow-wrap:anywhere]"
|
|
157
|
+
>
|
|
158
|
+
{title}
|
|
159
|
+
</h3>
|
|
160
|
+
{subtitle ? (
|
|
161
|
+
<p className="m-0 mt-0.5 break-words text-og-xs text-og-fg-muted [overflow-wrap:anywhere]">
|
|
162
|
+
{subtitle}
|
|
163
|
+
</p>
|
|
164
|
+
) : null}
|
|
165
|
+
</div>
|
|
166
|
+
</header>
|
|
167
|
+
{children ? (
|
|
168
|
+
<div
|
|
169
|
+
className={cn(
|
|
170
|
+
"min-w-0",
|
|
171
|
+
bare ? "pt-2" : "px-4 pb-3",
|
|
172
|
+
!bare && "sm:pl-[3.75rem]", // Aligns with the title, past the icon.
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
175
|
+
{children}
|
|
176
|
+
</div>
|
|
177
|
+
) : null}
|
|
178
|
+
{footer ? (
|
|
179
|
+
<footer
|
|
180
|
+
className={cn(
|
|
181
|
+
"flex flex-wrap items-center gap-x-3 gap-y-2",
|
|
182
|
+
bare
|
|
183
|
+
? "pt-3"
|
|
184
|
+
: waiting
|
|
185
|
+
? "border-t border-og-status-waiting/20 px-4 py-3"
|
|
186
|
+
: "border-t border-og-border px-4 py-3",
|
|
187
|
+
)}
|
|
188
|
+
>
|
|
189
|
+
{footer}
|
|
190
|
+
</footer>
|
|
191
|
+
) : null}
|
|
192
|
+
</article>
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const linkButton =
|
|
197
|
+
"focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40 inline-flex min-h-8 items-center gap-1 rounded-og-sm text-left text-og-sm text-og-fg underline decoration-og-border-strong underline-offset-4 hover:decoration-current [@media(pointer:coarse)]:min-h-11";
|
|
198
|
+
|
|
199
|
+
/** The facts of one saved action: consequences, selection and its key values. */
|
|
200
|
+
function ReviewFacts({
|
|
201
|
+
review,
|
|
202
|
+
onViewDetails,
|
|
203
|
+
showStatusDetail,
|
|
204
|
+
}: {
|
|
205
|
+
review: ToolActionReview;
|
|
206
|
+
onViewDetails?: ((path: string) => void) | undefined;
|
|
207
|
+
showStatusDetail: boolean;
|
|
208
|
+
}) {
|
|
209
|
+
const canOpen = Boolean(onViewDetails && review.detailsAvailable);
|
|
210
|
+
const selection =
|
|
211
|
+
review.selectionCount === undefined
|
|
212
|
+
? undefined
|
|
213
|
+
: review.fields.find((field) =>
|
|
214
|
+
["/messageIds", "/messageId", "/threadId"].includes(field.path),
|
|
215
|
+
);
|
|
216
|
+
const fields = selection
|
|
217
|
+
? review.fields.filter((field) => !selectionPaths.has(field.path))
|
|
218
|
+
: review.fields;
|
|
219
|
+
const shown = fields.slice(0, VISIBLE_FIELDS);
|
|
220
|
+
const hiddenFields = fields.length - shown.length + review.moreFields;
|
|
221
|
+
// The title already names the main effect; list only the remaining ones.
|
|
222
|
+
const effects = review.effects.filter(
|
|
223
|
+
(effect) => effect !== review.approveLabel && !review.title.startsWith(effect),
|
|
224
|
+
);
|
|
225
|
+
const note =
|
|
226
|
+
(showStatusDetail
|
|
227
|
+
? review.status === "blocked"
|
|
228
|
+
? `${review.reason || "Your permission settings block this action."} It did not run.`
|
|
229
|
+
: statusDetail[review.status]
|
|
230
|
+
: undefined) ?? review.consequence;
|
|
231
|
+
const samples = review.samples ?? [];
|
|
232
|
+
const count = review.selectionCount ?? 0;
|
|
233
|
+
const noun =
|
|
234
|
+
review.selectionKind === "threads"
|
|
235
|
+
? count === 1
|
|
236
|
+
? "conversation"
|
|
237
|
+
: "conversations"
|
|
238
|
+
: count === 1
|
|
239
|
+
? "message"
|
|
240
|
+
: "messages";
|
|
241
|
+
const hasContent =
|
|
242
|
+
note || effects.length || selection || shown.length || hiddenFields > 0 || samples.length;
|
|
243
|
+
if (!hasContent) return null;
|
|
244
|
+
return (
|
|
245
|
+
<div className="flex min-w-0 flex-col gap-3 text-og-sm">
|
|
246
|
+
{note ? <p className="m-0 leading-relaxed text-og-fg-muted">{note}</p> : null}
|
|
247
|
+
{effects.length > 0 ? (
|
|
248
|
+
<ul className="m-0 list-none space-y-1 p-0" aria-label="Also changes">
|
|
249
|
+
{effects.map((effect) => (
|
|
250
|
+
<li key={effect} className="flex items-start gap-2 text-og-fg">
|
|
251
|
+
<span
|
|
252
|
+
aria-hidden="true"
|
|
253
|
+
className="mt-[0.55em] size-1 shrink-0 rounded-full bg-og-fg-muted"
|
|
254
|
+
/>
|
|
255
|
+
<span className="min-w-0 break-words">{effect}</span>
|
|
256
|
+
</li>
|
|
257
|
+
))}
|
|
258
|
+
</ul>
|
|
259
|
+
) : null}
|
|
260
|
+
{selection ? (
|
|
261
|
+
<div className="min-w-0">
|
|
262
|
+
{samples.length > 0 ? (
|
|
263
|
+
<>
|
|
264
|
+
<p className="m-0 mb-1 text-og-xs text-og-fg-muted">Includes</p>
|
|
265
|
+
<ul className="m-0 list-none divide-y divide-og-border rounded-og-md border border-og-border bg-og-surface-1 p-0">
|
|
266
|
+
{samples.map((sample) => (
|
|
267
|
+
<li key={sample.id} className="min-w-0 px-3 py-2">
|
|
268
|
+
<p className="m-0 truncate text-og-sm text-og-fg">{sample.title}</p>
|
|
269
|
+
{sample.subtitle ? (
|
|
270
|
+
<p className="m-0 mt-0.5 truncate text-og-xs text-og-fg-muted">
|
|
271
|
+
{sample.subtitle}
|
|
272
|
+
</p>
|
|
273
|
+
) : null}
|
|
274
|
+
</li>
|
|
275
|
+
))}
|
|
276
|
+
{count > samples.length ? (
|
|
277
|
+
<li className="px-3 py-2 text-og-xs text-og-fg-muted">
|
|
278
|
+
and {(count - samples.length).toLocaleString()} more
|
|
279
|
+
</li>
|
|
280
|
+
) : null}
|
|
281
|
+
</ul>
|
|
282
|
+
</>
|
|
283
|
+
) : null}
|
|
284
|
+
{canOpen && count > 0 ? (
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
className={cn(linkButton, samples.length > 0 && "mt-1")}
|
|
288
|
+
data-review-path={selection.path}
|
|
289
|
+
onClick={() => onViewDetails!(selection.path)}
|
|
290
|
+
>
|
|
291
|
+
{count === 1 ? `View the ${noun}` : `View all ${count.toLocaleString()} ${noun}`}
|
|
292
|
+
<ChevronRightIcon className="size-3.5 shrink-0" aria-hidden="true" />
|
|
293
|
+
</button>
|
|
294
|
+
) : null}
|
|
295
|
+
</div>
|
|
296
|
+
) : null}
|
|
297
|
+
{shown.length > 0 ? (
|
|
298
|
+
<dl className="m-0 min-w-0 space-y-2">
|
|
299
|
+
{shown.map((field) => (
|
|
300
|
+
<div
|
|
301
|
+
key={field.path}
|
|
302
|
+
className="grid min-w-0 gap-x-4 gap-y-0.5 sm:grid-cols-[minmax(0,9rem)_minmax(0,1fr)]"
|
|
303
|
+
>
|
|
304
|
+
<dt className="m-0 break-words text-og-xs text-og-fg-muted sm:pt-px sm:text-og-sm">
|
|
305
|
+
{field.label}
|
|
306
|
+
</dt>
|
|
307
|
+
<dd className="m-0 min-w-0 whitespace-pre-wrap break-words text-og-fg [overflow-wrap:anywhere]">
|
|
308
|
+
{field.protected ? (
|
|
309
|
+
<span className="text-og-fg-muted">Hidden</span>
|
|
310
|
+
) : field.truncated && canOpen ? (
|
|
311
|
+
<button
|
|
312
|
+
type="button"
|
|
313
|
+
data-review-path={field.path}
|
|
314
|
+
aria-label={`View ${field.label}: ${field.preview}`}
|
|
315
|
+
onClick={() => onViewDetails!(field.path)}
|
|
316
|
+
className={cn(linkButton, "max-w-full")}
|
|
317
|
+
>
|
|
318
|
+
<span className="line-clamp-4 min-w-0 break-words [overflow-wrap:anywhere]">
|
|
319
|
+
{field.preview}
|
|
320
|
+
</span>
|
|
321
|
+
<ChevronRightIcon className="size-3.5 shrink-0" aria-hidden="true" />
|
|
322
|
+
</button>
|
|
323
|
+
) : (
|
|
324
|
+
field.preview
|
|
325
|
+
)}
|
|
326
|
+
</dd>
|
|
327
|
+
</div>
|
|
328
|
+
))}
|
|
329
|
+
</dl>
|
|
330
|
+
) : null}
|
|
331
|
+
{hiddenFields > 0 ? (
|
|
332
|
+
<p className="m-0 text-og-xs text-og-fg-muted">
|
|
333
|
+
{hiddenFields === 1 ? "1 more value" : `${hiddenFields} more values`} in details
|
|
334
|
+
</p>
|
|
335
|
+
) : null}
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function DeclineButton({
|
|
341
|
+
busy,
|
|
342
|
+
submitting,
|
|
343
|
+
label,
|
|
344
|
+
onClick,
|
|
345
|
+
}: {
|
|
346
|
+
busy: boolean;
|
|
347
|
+
submitting: boolean;
|
|
348
|
+
label?: string | undefined;
|
|
349
|
+
onClick: () => void;
|
|
350
|
+
}) {
|
|
351
|
+
return (
|
|
352
|
+
<button
|
|
353
|
+
type="button"
|
|
354
|
+
disabled={busy}
|
|
355
|
+
onClick={onClick}
|
|
356
|
+
className="inline-flex min-h-9 items-center justify-center rounded-og-md border border-og-border bg-og-surface-1 px-3 py-1.5 text-og-sm font-medium text-og-fg transition-colors hover:border-og-border-strong hover:bg-og-hover disabled:opacity-60 [@media(pointer:coarse)]:min-h-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
357
|
+
>
|
|
358
|
+
{submitting ? "Declining…" : (label ?? "Decline")}
|
|
359
|
+
</button>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** One host-neutral review. All callbacks refer to the immutable action; none can edit it. */
|
|
364
|
+
export function ToolActionReviewCard({
|
|
365
|
+
review,
|
|
366
|
+
onApprove,
|
|
367
|
+
onReject,
|
|
368
|
+
submitting,
|
|
369
|
+
disabled,
|
|
370
|
+
onViewDetails,
|
|
371
|
+
pendingLabel,
|
|
372
|
+
rejectLabel,
|
|
373
|
+
bare,
|
|
374
|
+
}: {
|
|
375
|
+
review: ToolActionReview;
|
|
376
|
+
onApprove?: (() => void) | undefined;
|
|
377
|
+
onReject?: (() => void) | undefined;
|
|
378
|
+
submitting?: "approve" | "reject" | null | undefined;
|
|
379
|
+
disabled?: boolean | undefined;
|
|
380
|
+
onViewDetails?: ((path: string) => void) | undefined;
|
|
381
|
+
pendingLabel?: string | undefined;
|
|
382
|
+
rejectLabel?: string | undefined;
|
|
383
|
+
/** Render without card chrome, e.g. inside an expanded timeline row. */
|
|
384
|
+
bare?: boolean | undefined;
|
|
385
|
+
}) {
|
|
386
|
+
const titleId = useId();
|
|
387
|
+
const canApprove = review.availableActions.includes("approve") && !!onApprove;
|
|
388
|
+
const canReject = review.availableActions.includes("reject") && !!onReject;
|
|
389
|
+
const actionable = review.status === "pending" && (canApprove || canReject);
|
|
390
|
+
const busy = Boolean(disabled || submitting);
|
|
391
|
+
const tone: Tone = statusTone[review.status];
|
|
392
|
+
const eyebrow = submitting
|
|
393
|
+
? submitting === "approve"
|
|
394
|
+
? "Approving…"
|
|
395
|
+
: "Declining…"
|
|
396
|
+
: review.status === "pending"
|
|
397
|
+
? (pendingLabel ?? statusText.pending)
|
|
398
|
+
: statusText[review.status];
|
|
399
|
+
const detailsLink =
|
|
400
|
+
review.detailsAvailable && onViewDetails ? (
|
|
401
|
+
<button
|
|
402
|
+
type="button"
|
|
403
|
+
className="inline-flex min-h-8 items-center rounded-og-sm text-og-sm text-og-fg-muted underline decoration-og-border-strong underline-offset-4 hover:text-og-fg hover:decoration-current [@media(pointer:coarse)]:min-h-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
404
|
+
data-review-path=""
|
|
405
|
+
onClick={() => onViewDetails("")}
|
|
406
|
+
>
|
|
407
|
+
Details
|
|
408
|
+
</button>
|
|
409
|
+
) : null;
|
|
410
|
+
const reason = review.reason.trim();
|
|
411
|
+
return (
|
|
412
|
+
<ToolReviewFrame
|
|
413
|
+
tone={tone}
|
|
414
|
+
icon={<StatusIcon status={review.status} busy={Boolean(submitting)} />}
|
|
415
|
+
eyebrow={eyebrow}
|
|
416
|
+
title={review.title}
|
|
417
|
+
titleId={titleId}
|
|
418
|
+
subtitle={review.accountLabel}
|
|
419
|
+
bare={bare}
|
|
420
|
+
aria-busy={Boolean(submitting)}
|
|
421
|
+
data-og-tool-review=""
|
|
422
|
+
data-review-status={review.status}
|
|
423
|
+
footer={
|
|
424
|
+
actionable ? (
|
|
425
|
+
<>
|
|
426
|
+
<div className="flex min-w-0 flex-1 basis-48 flex-wrap items-center gap-x-3 gap-y-1">
|
|
427
|
+
{detailsLink}
|
|
428
|
+
{reason ? (
|
|
429
|
+
<span className="min-w-0 text-og-xs leading-relaxed text-og-fg-subtle">
|
|
430
|
+
{reason}
|
|
431
|
+
</span>
|
|
432
|
+
) : null}
|
|
433
|
+
</div>
|
|
434
|
+
<div className="ml-auto flex shrink-0 flex-wrap items-center justify-end gap-2">
|
|
435
|
+
{canReject ? (
|
|
436
|
+
<DeclineButton
|
|
437
|
+
busy={busy}
|
|
438
|
+
submitting={submitting === "reject"}
|
|
439
|
+
label={rejectLabel}
|
|
440
|
+
onClick={onReject!}
|
|
441
|
+
/>
|
|
442
|
+
) : null}
|
|
443
|
+
{canApprove ? (
|
|
444
|
+
<button
|
|
445
|
+
type="button"
|
|
446
|
+
disabled={busy}
|
|
447
|
+
onClick={onApprove}
|
|
448
|
+
className="inline-flex min-h-9 items-center justify-center gap-2 rounded-og-md border border-og-primary-border bg-og-primary px-3.5 py-1.5 text-og-sm font-medium text-og-primary-fg transition-colors hover:bg-og-primary-hover disabled:opacity-60 [@media(pointer:coarse)]:min-h-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
449
|
+
>
|
|
450
|
+
{submitting === "approve" ? (
|
|
451
|
+
<LoaderCircleIcon className="size-3.5 animate-spin" aria-hidden="true" />
|
|
452
|
+
) : null}
|
|
453
|
+
{submitting === "approve" ? "Approving…" : review.approveLabel}
|
|
454
|
+
</button>
|
|
455
|
+
) : null}
|
|
456
|
+
</div>
|
|
457
|
+
</>
|
|
458
|
+
) : undefined
|
|
459
|
+
}
|
|
460
|
+
>
|
|
461
|
+
{actionable && !canApprove ? (
|
|
462
|
+
<p className="m-0 mb-3 text-og-sm leading-relaxed text-og-fg">{UNRECOVERABLE_TEXT}</p>
|
|
463
|
+
) : null}
|
|
464
|
+
<ReviewFacts
|
|
465
|
+
review={review}
|
|
466
|
+
onViewDetails={onViewDetails}
|
|
467
|
+
showStatusDetail={review.status !== "pending"}
|
|
468
|
+
/>
|
|
469
|
+
{!actionable && detailsLink ? <div className="mt-2">{detailsLink}</div> : null}
|
|
470
|
+
</ToolReviewFrame>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Full-page detail content: hosts choose navigation, never a nested scrolling panel. */
|
|
475
|
+
export function ToolActionReviewDetails({
|
|
476
|
+
review,
|
|
477
|
+
path: initialPath = "",
|
|
478
|
+
load,
|
|
479
|
+
onBack,
|
|
480
|
+
manageFocus = true,
|
|
481
|
+
}: {
|
|
482
|
+
review: ToolActionReview;
|
|
483
|
+
path?: string;
|
|
484
|
+
load: ToolReviewDetailsLoader;
|
|
485
|
+
onBack: () => void;
|
|
486
|
+
/** Hosts rendering simultaneous previews can retain their own focus. */
|
|
487
|
+
manageFocus?: boolean;
|
|
488
|
+
}) {
|
|
489
|
+
const [path, setPath] = useState(initialPath);
|
|
490
|
+
const [trail, setTrail] = useState<Array<{ path: string; label: string }>>([]);
|
|
491
|
+
const [offset, setOffset] = useState(0);
|
|
492
|
+
const [page, setPage] = useState<ToolReviewDetailsPage | null>(null);
|
|
493
|
+
const [error, setError] = useState(false);
|
|
494
|
+
const [retry, setRetry] = useState(0);
|
|
495
|
+
const heading = useRef<HTMLHeadingElement>(null);
|
|
496
|
+
const focusedOnce = useRef(false);
|
|
497
|
+
useEffect(() => {
|
|
498
|
+
if (page && manageFocus && !focusedOnce.current) {
|
|
499
|
+
focusedOnce.current = true;
|
|
500
|
+
heading.current?.focus({ preventScroll: true });
|
|
501
|
+
heading.current?.scrollIntoView({ block: "start" });
|
|
502
|
+
}
|
|
503
|
+
}, [page, manageFocus]);
|
|
504
|
+
useEffect(() => {
|
|
505
|
+
let active = true;
|
|
506
|
+
setPage(null);
|
|
507
|
+
setError(false);
|
|
508
|
+
void load(review, path, offset).then(
|
|
509
|
+
(value) => {
|
|
510
|
+
if (!active) return;
|
|
511
|
+
if (
|
|
512
|
+
value.actionDigest !== review.actionDigest ||
|
|
513
|
+
value.id !== review.id ||
|
|
514
|
+
value.path !== path
|
|
515
|
+
) {
|
|
516
|
+
setError(true);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
setPage(value);
|
|
520
|
+
},
|
|
521
|
+
() => {
|
|
522
|
+
if (active) setError(true);
|
|
523
|
+
},
|
|
524
|
+
);
|
|
525
|
+
return () => {
|
|
526
|
+
active = false;
|
|
527
|
+
};
|
|
528
|
+
}, [review, path, offset, load, retry]);
|
|
529
|
+
const open = (next: string, label: string) => {
|
|
530
|
+
setTrail((value) => [...value, { path, label }]);
|
|
531
|
+
setPath(next);
|
|
532
|
+
setOffset(0);
|
|
533
|
+
};
|
|
534
|
+
const up = () => {
|
|
535
|
+
const previous = trail.at(-1);
|
|
536
|
+
setTrail((value) => value.slice(0, -1));
|
|
537
|
+
setPath(previous?.path ?? initialPath);
|
|
538
|
+
setOffset(0);
|
|
539
|
+
};
|
|
540
|
+
return (
|
|
541
|
+
<section
|
|
542
|
+
className="og-root mx-auto w-full max-w-3xl px-4 py-5 text-og-fg sm:px-6"
|
|
543
|
+
data-og-review-details=""
|
|
544
|
+
>
|
|
545
|
+
<button
|
|
546
|
+
type="button"
|
|
547
|
+
onClick={onBack}
|
|
548
|
+
className="mb-4 inline-flex min-h-11 items-center gap-2 rounded-og-md pr-2 text-og-sm text-og-fg-muted hover:text-og-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
549
|
+
>
|
|
550
|
+
<ArrowLeftIcon className="size-4" aria-hidden="true" />
|
|
551
|
+
Back to review
|
|
552
|
+
</button>
|
|
553
|
+
<h2
|
|
554
|
+
ref={heading}
|
|
555
|
+
tabIndex={-1}
|
|
556
|
+
className="m-0 break-words text-og-lg font-semibold outline-hidden [overflow-wrap:anywhere]"
|
|
557
|
+
>
|
|
558
|
+
{review.title}
|
|
559
|
+
</h2>
|
|
560
|
+
{review.accountLabel ? (
|
|
561
|
+
<p className="m-0 mt-1 text-og-sm text-og-fg-muted">{review.accountLabel}</p>
|
|
562
|
+
) : null}
|
|
563
|
+
<p className="m-0 mt-2 text-og-xs text-og-fg-subtle">
|
|
564
|
+
The exact values saved for this action. Passwords and keys stay hidden.
|
|
565
|
+
</p>
|
|
566
|
+
{trail.length > 0 ? (
|
|
567
|
+
<button
|
|
568
|
+
type="button"
|
|
569
|
+
className="mt-3 inline-flex min-h-11 items-center gap-1.5 text-og-sm text-og-fg-muted underline underline-offset-4 hover:text-og-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
570
|
+
onClick={up}
|
|
571
|
+
>
|
|
572
|
+
<ArrowLeftIcon className="size-3.5" aria-hidden="true" />
|
|
573
|
+
Back to {trail.at(-1)?.label ?? "all values"}
|
|
574
|
+
</button>
|
|
575
|
+
) : null}
|
|
576
|
+
{error ? (
|
|
577
|
+
<div role="alert" className="mt-4 py-4 text-og-sm">
|
|
578
|
+
Couldn't load these details.{" "}
|
|
579
|
+
<button
|
|
580
|
+
type="button"
|
|
581
|
+
onClick={() => setRetry((value) => value + 1)}
|
|
582
|
+
className="min-h-11 underline underline-offset-4 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
583
|
+
>
|
|
584
|
+
Try again
|
|
585
|
+
</button>
|
|
586
|
+
</div>
|
|
587
|
+
) : !page ? (
|
|
588
|
+
<p role="status" className="mt-4 py-4 text-og-sm text-og-fg-muted">
|
|
589
|
+
Loading details…
|
|
590
|
+
</p>
|
|
591
|
+
) : (
|
|
592
|
+
<>
|
|
593
|
+
<dl className="m-0 mt-4 divide-y divide-og-border border-y border-og-border">
|
|
594
|
+
{page.items.map((item) => (
|
|
595
|
+
<div key={`${page.path}:${item.label}`} className="py-3">
|
|
596
|
+
<dt className="m-0 text-og-xs text-og-fg-muted">{item.label}</dt>
|
|
597
|
+
<dd className="m-0 mt-1 whitespace-pre-wrap break-words text-og-sm leading-relaxed [overflow-wrap:anywhere]">
|
|
598
|
+
{item.truncated && item.path ? (
|
|
599
|
+
<button
|
|
600
|
+
type="button"
|
|
601
|
+
aria-label={`Open ${item.label}`}
|
|
602
|
+
className={cn(linkButton, "max-w-full")}
|
|
603
|
+
onClick={() => open(item.path!, item.label)}
|
|
604
|
+
>
|
|
605
|
+
<span className="min-w-0 break-words focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40">
|
|
606
|
+
{item.value}
|
|
607
|
+
</span>
|
|
608
|
+
<ChevronRightIcon className="size-3.5 shrink-0" aria-hidden="true" />
|
|
609
|
+
</button>
|
|
610
|
+
) : (
|
|
611
|
+
item.value
|
|
612
|
+
)}
|
|
613
|
+
</dd>
|
|
614
|
+
</div>
|
|
615
|
+
))}
|
|
616
|
+
</dl>
|
|
617
|
+
{page.total > page.items.length || offset > 0 ? (
|
|
618
|
+
<nav
|
|
619
|
+
aria-label="Detail pages"
|
|
620
|
+
className="mt-4 flex flex-wrap items-center justify-between gap-3 text-og-sm"
|
|
621
|
+
>
|
|
622
|
+
<button
|
|
623
|
+
type="button"
|
|
624
|
+
disabled={offset === 0}
|
|
625
|
+
onClick={() => setOffset(Math.max(0, offset - 25))}
|
|
626
|
+
className="min-h-11 rounded-og-md border border-og-border px-3 hover:bg-og-hover disabled:opacity-40 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
627
|
+
>
|
|
628
|
+
Previous
|
|
629
|
+
</button>
|
|
630
|
+
<span className="text-og-fg-muted tabular-nums">
|
|
631
|
+
{page.total === 0
|
|
632
|
+
? "No items"
|
|
633
|
+
: `${(offset + 1).toLocaleString()}–${(offset + page.items.length).toLocaleString()} of ${page.total.toLocaleString()}`}
|
|
634
|
+
</span>
|
|
635
|
+
<button
|
|
636
|
+
type="button"
|
|
637
|
+
disabled={page.nextOffset === null}
|
|
638
|
+
onClick={() => setOffset(page.nextOffset ?? offset)}
|
|
639
|
+
className="min-h-11 rounded-og-md border border-og-border px-3 hover:bg-og-hover disabled:opacity-40 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
|
|
640
|
+
>
|
|
641
|
+
Next
|
|
642
|
+
</button>
|
|
643
|
+
</nav>
|
|
644
|
+
) : null}
|
|
645
|
+
</>
|
|
646
|
+
)}
|
|
647
|
+
</section>
|
|
648
|
+
);
|
|
649
|
+
}
|