@opengeni/react 1.2.1-canary.37222177070001 → 1.3.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 +31 -0
- package/dist/{chunk-4OTO7KBK.js → chunk-PGSX7UCP.js} +2062 -1489
- package/dist/chunk-PGSX7UCP.js.map +1 -0
- package/dist/{chunk-X34BSY2R.js → chunk-WWEUO2PL.js} +60 -60
- package/dist/chunk-WWEUO2PL.js.map +1 -0
- package/dist/components/approval-surface.d.ts +11 -13
- package/dist/components/open-geni-chat.d.ts +21 -6
- package/dist/components/session-conversation.d.ts +18 -1
- package/dist/components/session-proxy-scope.d.ts +16 -2
- package/dist/components/tool-action-review.d.ts +20 -0
- package/dist/components/tool-review-history.d.ts +21 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/session-ui.d.ts +4 -1
- package/dist/session-ui.js +8 -2
- package/dist/session-ui.js.map +1 -1
- package/dist/session.js +1 -1
- package/package.json +3 -3
- package/src/components/approval-surface.tsx +263 -245
- package/src/components/open-geni-chat.tsx +197 -64
- package/src/components/session-conversation.tsx +26 -7
- package/src/components/session-proxy-scope.tsx +47 -7
- package/src/components/tool-action-review.tsx +386 -0
- package/src/components/tool-review-history.tsx +113 -0
- package/src/index.ts +9 -1
- package/src/session-ui.ts +9 -1
- package/src/timeline/tool-renderers.tsx +7 -0
- package/styles/compiled.css +33 -13
- package/dist/chunk-4OTO7KBK.js.map +0 -1
- package/dist/chunk-X34BSY2R.js.map +0 -1
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { ArrowLeftIcon, CheckIcon, ChevronRightIcon, LoaderCircleIcon } from "lucide-react";
|
|
2
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
3
|
+
import type { ToolActionReview, ToolReviewDetailsPage, ToolReviewStatus } from "@opengeni/sdk";
|
|
4
|
+
|
|
5
|
+
export type ToolReviewDetailsLoader = (
|
|
6
|
+
review: ToolActionReview,
|
|
7
|
+
path: string,
|
|
8
|
+
offset: number,
|
|
9
|
+
) => Promise<ToolReviewDetailsPage>;
|
|
10
|
+
const statusText: Record<ToolReviewStatus, string> = {
|
|
11
|
+
pending: "Needs your approval",
|
|
12
|
+
approved: "Approved · waiting to run",
|
|
13
|
+
executing: "In progress",
|
|
14
|
+
completed: "Completed",
|
|
15
|
+
partial: "Partially completed",
|
|
16
|
+
unknown: "Outcome uncertain",
|
|
17
|
+
rejected: "Declined",
|
|
18
|
+
cancelled: "Cancelled",
|
|
19
|
+
expired: "Review expired",
|
|
20
|
+
revoked: "Access unavailable",
|
|
21
|
+
stale: "Action changed",
|
|
22
|
+
failed: "Action failed",
|
|
23
|
+
unavailable: "Details unavailable",
|
|
24
|
+
};
|
|
25
|
+
const statusDetail: Partial<Record<ToolReviewStatus, string>> = {
|
|
26
|
+
unknown: "Execution may have happened. Check the result before trying again.",
|
|
27
|
+
rejected: "This action was not run.",
|
|
28
|
+
cancelled: "This action will not run.",
|
|
29
|
+
expired: "Prepare a new action to continue.",
|
|
30
|
+
revoked: "Restore access before preparing a new action.",
|
|
31
|
+
stale: "The tool or account changed. A new review is needed.",
|
|
32
|
+
partial: "Some work completed. Review the results before continuing.",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** One host-neutral review. All callbacks refer to the immutable action; none can edit it. */
|
|
36
|
+
export function ToolActionReviewCard({
|
|
37
|
+
review,
|
|
38
|
+
onApprove,
|
|
39
|
+
onReject,
|
|
40
|
+
submitting,
|
|
41
|
+
disabled,
|
|
42
|
+
onViewDetails,
|
|
43
|
+
pendingLabel,
|
|
44
|
+
rejectLabel,
|
|
45
|
+
}: {
|
|
46
|
+
review: ToolActionReview;
|
|
47
|
+
onApprove?: (() => void) | undefined;
|
|
48
|
+
onReject?: (() => void) | undefined;
|
|
49
|
+
submitting?: "approve" | "reject" | null | undefined;
|
|
50
|
+
disabled?: boolean | undefined;
|
|
51
|
+
onViewDetails?: ((path: string) => void) | undefined;
|
|
52
|
+
pendingLabel?: string | undefined;
|
|
53
|
+
rejectLabel?: string | undefined;
|
|
54
|
+
}) {
|
|
55
|
+
const titleId = useId();
|
|
56
|
+
const actionable = review.status === "pending" && review.availableActions.includes("approve");
|
|
57
|
+
const quiet =
|
|
58
|
+
review.status === "completed" || review.status === "rejected" || review.status === "cancelled";
|
|
59
|
+
const busy = Boolean(disabled || submitting);
|
|
60
|
+
const selection =
|
|
61
|
+
review.selectionCount === undefined
|
|
62
|
+
? undefined
|
|
63
|
+
: review.fields.find(
|
|
64
|
+
(field) =>
|
|
65
|
+
field.path === "/messageIds" ||
|
|
66
|
+
field.path === "/messageId" ||
|
|
67
|
+
field.path === "/threadId",
|
|
68
|
+
);
|
|
69
|
+
const fields = selection
|
|
70
|
+
? review.fields.filter(
|
|
71
|
+
(field) =>
|
|
72
|
+
![selection.path, "/addLabelIds", "/removeLabelIds", "/labelIds"].includes(field.path),
|
|
73
|
+
)
|
|
74
|
+
: review.fields;
|
|
75
|
+
const extraEffects = review.effects.filter((effect) => effect !== review.approveLabel);
|
|
76
|
+
return (
|
|
77
|
+
<article
|
|
78
|
+
aria-labelledby={titleId}
|
|
79
|
+
aria-busy={Boolean(submitting)}
|
|
80
|
+
className="min-w-0 py-4"
|
|
81
|
+
data-og-tool-review=""
|
|
82
|
+
data-review-status={review.status}
|
|
83
|
+
>
|
|
84
|
+
<div className="flex min-w-0 items-center gap-2 text-og-xs text-og-fg-muted" role="status">
|
|
85
|
+
{review.status === "completed" ? (
|
|
86
|
+
<CheckIcon className="size-3.5" aria-hidden="true" />
|
|
87
|
+
) : (
|
|
88
|
+
<span className="size-1.5 shrink-0 rounded-full bg-current" aria-hidden="true" />
|
|
89
|
+
)}
|
|
90
|
+
<span>
|
|
91
|
+
{submitting
|
|
92
|
+
? submitting === "approve"
|
|
93
|
+
? "Approving…"
|
|
94
|
+
: "Declining…"
|
|
95
|
+
: review.status === "pending"
|
|
96
|
+
? (pendingLabel ?? statusText.pending)
|
|
97
|
+
: statusText[review.status]}
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
<h3
|
|
101
|
+
tabIndex={-1}
|
|
102
|
+
id={titleId}
|
|
103
|
+
className="m-0 mt-1.5 outline-hidden break-words text-og-md font-semibold leading-snug text-og-fg"
|
|
104
|
+
>
|
|
105
|
+
{review.title}
|
|
106
|
+
</h3>
|
|
107
|
+
{review.accountLabel && (
|
|
108
|
+
<p className="m-0 mt-1 break-words text-og-sm text-og-fg-muted">{review.accountLabel}</p>
|
|
109
|
+
)}
|
|
110
|
+
{(statusDetail[review.status] || review.consequence) && (
|
|
111
|
+
<p className="m-0 mt-3 text-og-sm leading-relaxed text-og-fg-muted">
|
|
112
|
+
{statusDetail[review.status] ?? review.consequence}
|
|
113
|
+
</p>
|
|
114
|
+
)}
|
|
115
|
+
{!quiet && (
|
|
116
|
+
<>
|
|
117
|
+
{extraEffects.length > 0 && (
|
|
118
|
+
<ul className="m-0 mt-3 list-none space-y-1 p-0 text-og-sm text-og-fg">
|
|
119
|
+
{extraEffects.map((effect) => (
|
|
120
|
+
<li key={effect}>{effect}</li>
|
|
121
|
+
))}
|
|
122
|
+
</ul>
|
|
123
|
+
)}
|
|
124
|
+
{selection && (
|
|
125
|
+
<div className="mt-4">
|
|
126
|
+
{review.samples && review.samples.length > 0 && (
|
|
127
|
+
<>
|
|
128
|
+
<p className="m-0 mb-1 text-og-xs text-og-fg-muted">
|
|
129
|
+
Examples from this selection
|
|
130
|
+
</p>
|
|
131
|
+
<ul className="m-0 list-none divide-y divide-og-border p-0">
|
|
132
|
+
{review.samples.map((sample) => (
|
|
133
|
+
<li key={sample.id} className="py-2">
|
|
134
|
+
<p className="m-0 break-words text-og-sm text-og-fg">{sample.title}</p>
|
|
135
|
+
{sample.subtitle && (
|
|
136
|
+
<p className="m-0 mt-0.5 break-words text-og-xs text-og-fg-muted">
|
|
137
|
+
{sample.subtitle}
|
|
138
|
+
</p>
|
|
139
|
+
)}
|
|
140
|
+
</li>
|
|
141
|
+
))}
|
|
142
|
+
</ul>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
{onViewDetails && review.detailsAvailable && (
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
className="inline-flex min-h-11 items-center gap-1 text-og-sm text-og-fg underline decoration-og-border-strong underline-offset-4 hover:decoration-current"
|
|
149
|
+
data-review-path={selection.path}
|
|
150
|
+
onClick={() => onViewDetails(selection.path)}
|
|
151
|
+
>
|
|
152
|
+
View all {review.selectionCount?.toLocaleString()}{" "}
|
|
153
|
+
{review.selectionKind ?? "messages"}
|
|
154
|
+
<ChevronRightIcon className="size-3.5" aria-hidden="true" />
|
|
155
|
+
</button>
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
<dl className="m-0 mt-3 divide-y divide-og-border text-og-sm">
|
|
160
|
+
{fields.map((field) => (
|
|
161
|
+
<div
|
|
162
|
+
key={field.path}
|
|
163
|
+
className="grid min-w-0 grid-cols-[minmax(0,1fr)_minmax(0,2fr)] gap-x-4 py-2"
|
|
164
|
+
>
|
|
165
|
+
<dt className="m-0 break-words text-og-fg-muted">{field.label}</dt>
|
|
166
|
+
<dd className="m-0 min-w-0 whitespace-pre-wrap break-words text-og-fg [overflow-wrap:anywhere]">
|
|
167
|
+
{field.truncated &&
|
|
168
|
+
!field.protected &&
|
|
169
|
+
onViewDetails &&
|
|
170
|
+
review.detailsAvailable ? (
|
|
171
|
+
<button
|
|
172
|
+
type="button"
|
|
173
|
+
data-review-path={field.path}
|
|
174
|
+
onClick={() => onViewDetails(field.path)}
|
|
175
|
+
className="inline-flex min-h-8 max-w-full items-center gap-1 text-left underline decoration-og-border-strong underline-offset-4 hover:decoration-current [@media(pointer:coarse)]:min-h-11"
|
|
176
|
+
>
|
|
177
|
+
<span className="min-w-0 break-words">{field.preview}</span>
|
|
178
|
+
<ChevronRightIcon className="size-3.5 shrink-0" aria-hidden="true" />
|
|
179
|
+
</button>
|
|
180
|
+
) : (
|
|
181
|
+
field.preview
|
|
182
|
+
)}
|
|
183
|
+
</dd>
|
|
184
|
+
</div>
|
|
185
|
+
))}
|
|
186
|
+
</dl>
|
|
187
|
+
{review.moreFields > 0 && (
|
|
188
|
+
<p className="m-0 mt-2 text-og-xs text-og-fg-muted">
|
|
189
|
+
{review.moreFields} more fields in the full details
|
|
190
|
+
</p>
|
|
191
|
+
)}
|
|
192
|
+
</>
|
|
193
|
+
)}
|
|
194
|
+
{review.detailsAvailable && onViewDetails && (
|
|
195
|
+
<button
|
|
196
|
+
type="button"
|
|
197
|
+
className="mt-1 min-h-8 text-og-xs text-og-fg-muted underline underline-offset-4 hover:text-og-fg [@media(pointer:coarse)]:min-h-11"
|
|
198
|
+
data-review-path=""
|
|
199
|
+
onClick={() => onViewDetails("")}
|
|
200
|
+
>
|
|
201
|
+
View full details
|
|
202
|
+
</button>
|
|
203
|
+
)}
|
|
204
|
+
{actionable && (
|
|
205
|
+
<>
|
|
206
|
+
<p className="m-0 mt-3 text-og-xs leading-relaxed text-og-fg-muted">{review.reason}</p>
|
|
207
|
+
<div className="mt-4 flex flex-wrap items-center gap-2">
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
disabled={busy}
|
|
211
|
+
onClick={onApprove}
|
|
212
|
+
className="inline-flex min-h-9 items-center justify-center gap-2 rounded-og-md border border-og-primary-border bg-og-primary px-4 py-2 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"
|
|
213
|
+
>
|
|
214
|
+
{submitting === "approve" && (
|
|
215
|
+
<LoaderCircleIcon className="size-3.5 animate-spin" aria-hidden="true" />
|
|
216
|
+
)}
|
|
217
|
+
{submitting === "approve" ? "Approving…" : review.approveLabel}
|
|
218
|
+
</button>
|
|
219
|
+
<button
|
|
220
|
+
type="button"
|
|
221
|
+
disabled={busy}
|
|
222
|
+
onClick={onReject}
|
|
223
|
+
className="min-h-9 rounded-og-md px-3 py-2 text-og-sm text-og-fg-muted transition-colors hover:bg-og-fg/5 hover:text-og-fg disabled:opacity-60 [@media(pointer:coarse)]:min-h-11"
|
|
224
|
+
>
|
|
225
|
+
{submitting === "reject" ? "Declining…" : (rejectLabel ?? "Decline")}
|
|
226
|
+
</button>
|
|
227
|
+
</div>
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
</article>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Full-page detail content: hosts choose navigation, never a nested scrolling panel. */
|
|
235
|
+
export function ToolActionReviewDetails({
|
|
236
|
+
review,
|
|
237
|
+
path: initialPath = "",
|
|
238
|
+
load,
|
|
239
|
+
onBack,
|
|
240
|
+
}: {
|
|
241
|
+
review: ToolActionReview;
|
|
242
|
+
path?: string;
|
|
243
|
+
load: ToolReviewDetailsLoader;
|
|
244
|
+
onBack: () => void;
|
|
245
|
+
}) {
|
|
246
|
+
const [path, setPath] = useState(initialPath);
|
|
247
|
+
const [offset, setOffset] = useState(0);
|
|
248
|
+
const [page, setPage] = useState<ToolReviewDetailsPage | null>(null);
|
|
249
|
+
const [error, setError] = useState(false);
|
|
250
|
+
const [retry, setRetry] = useState(0);
|
|
251
|
+
const heading = useRef<HTMLHeadingElement>(null);
|
|
252
|
+
useEffect(() => {
|
|
253
|
+
if (page) {
|
|
254
|
+
heading.current?.focus({ preventScroll: true });
|
|
255
|
+
heading.current?.scrollIntoView({ block: "start" });
|
|
256
|
+
}
|
|
257
|
+
}, [page]);
|
|
258
|
+
useEffect(() => {
|
|
259
|
+
let active = true;
|
|
260
|
+
setPage(null);
|
|
261
|
+
setError(false);
|
|
262
|
+
void load(review, path, offset).then(
|
|
263
|
+
(value) => {
|
|
264
|
+
if (!active) return;
|
|
265
|
+
if (
|
|
266
|
+
value.actionDigest !== review.actionDigest ||
|
|
267
|
+
value.id !== review.id ||
|
|
268
|
+
value.path !== path
|
|
269
|
+
) {
|
|
270
|
+
setError(true);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
setPage(value);
|
|
274
|
+
},
|
|
275
|
+
() => {
|
|
276
|
+
if (active) setError(true);
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
return () => {
|
|
280
|
+
active = false;
|
|
281
|
+
};
|
|
282
|
+
}, [review, path, offset, load, retry]);
|
|
283
|
+
return (
|
|
284
|
+
<section className="og-root mx-auto w-full max-w-3xl p-6 text-og-fg" data-og-review-details="">
|
|
285
|
+
<button
|
|
286
|
+
type="button"
|
|
287
|
+
onClick={onBack}
|
|
288
|
+
className="mb-5 inline-flex min-h-11 items-center gap-2 text-og-sm text-og-fg-muted hover:text-og-fg"
|
|
289
|
+
>
|
|
290
|
+
<ArrowLeftIcon className="size-4" aria-hidden="true" />
|
|
291
|
+
Back to review
|
|
292
|
+
</button>
|
|
293
|
+
<h2
|
|
294
|
+
ref={heading}
|
|
295
|
+
tabIndex={-1}
|
|
296
|
+
className="m-0 break-words text-og-lg font-semibold outline-hidden"
|
|
297
|
+
>
|
|
298
|
+
{review.title}
|
|
299
|
+
</h2>
|
|
300
|
+
{review.accountLabel && <p className="text-og-sm text-og-fg-muted">{review.accountLabel}</p>}
|
|
301
|
+
<p className="text-og-sm text-og-fg-muted">
|
|
302
|
+
Exact saved selection and values. Protected credentials stay hidden.
|
|
303
|
+
</p>
|
|
304
|
+
{path !== initialPath && (
|
|
305
|
+
<button
|
|
306
|
+
type="button"
|
|
307
|
+
className="min-h-11 text-og-sm underline"
|
|
308
|
+
onClick={() => {
|
|
309
|
+
setPath(initialPath);
|
|
310
|
+
setOffset(0);
|
|
311
|
+
}}
|
|
312
|
+
>
|
|
313
|
+
Back to all fields
|
|
314
|
+
</button>
|
|
315
|
+
)}
|
|
316
|
+
{error ? (
|
|
317
|
+
<div role="alert" className="py-6 text-og-sm">
|
|
318
|
+
Details could not be loaded.{" "}
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
onClick={() => setRetry((value) => value + 1)}
|
|
322
|
+
className="min-h-11 underline"
|
|
323
|
+
>
|
|
324
|
+
Try again
|
|
325
|
+
</button>
|
|
326
|
+
</div>
|
|
327
|
+
) : !page ? (
|
|
328
|
+
<p role="status" className="py-6 text-og-sm text-og-fg-muted">
|
|
329
|
+
Loading details…
|
|
330
|
+
</p>
|
|
331
|
+
) : (
|
|
332
|
+
<>
|
|
333
|
+
<dl className="m-0 divide-y divide-og-border">
|
|
334
|
+
{page.items.map((item) => (
|
|
335
|
+
<div key={`${page.path}:${item.label}`} className="py-4">
|
|
336
|
+
<dt className="m-0 text-og-xs text-og-fg-muted">{item.label}</dt>
|
|
337
|
+
<dd className="m-0 mt-1 whitespace-pre-wrap break-words text-og-sm leading-relaxed [overflow-wrap:anywhere]">
|
|
338
|
+
{item.value}
|
|
339
|
+
{item.truncated && item.path && (
|
|
340
|
+
<button
|
|
341
|
+
type="button"
|
|
342
|
+
aria-label={`Open complete ${item.label}`}
|
|
343
|
+
className="mt-2 block min-h-11 text-og-sm underline underline-offset-4"
|
|
344
|
+
onClick={() => {
|
|
345
|
+
setPath(item.path!);
|
|
346
|
+
setOffset(0);
|
|
347
|
+
}}
|
|
348
|
+
>
|
|
349
|
+
Open complete field
|
|
350
|
+
</button>
|
|
351
|
+
)}
|
|
352
|
+
</dd>
|
|
353
|
+
</div>
|
|
354
|
+
))}
|
|
355
|
+
</dl>
|
|
356
|
+
<nav
|
|
357
|
+
aria-label="Review detail pages"
|
|
358
|
+
className="mt-6 flex flex-wrap items-center justify-between gap-3 border-t border-og-border pt-4 text-og-sm"
|
|
359
|
+
>
|
|
360
|
+
<button
|
|
361
|
+
type="button"
|
|
362
|
+
disabled={offset === 0}
|
|
363
|
+
onClick={() => setOffset(Math.max(0, offset - 25))}
|
|
364
|
+
className="min-h-11 rounded-og-md border border-og-border px-3 disabled:opacity-40"
|
|
365
|
+
>
|
|
366
|
+
Previous
|
|
367
|
+
</button>
|
|
368
|
+
<span className="text-og-fg-muted">
|
|
369
|
+
{page.total === 0
|
|
370
|
+
? "No items"
|
|
371
|
+
: `${offset + 1}–${offset + page.items.length} of ${page.total.toLocaleString()}`}
|
|
372
|
+
</span>
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
disabled={page.nextOffset === null}
|
|
376
|
+
onClick={() => setOffset(page.nextOffset ?? offset)}
|
|
377
|
+
className="min-h-11 rounded-og-md border border-og-border px-3 disabled:opacity-40"
|
|
378
|
+
>
|
|
379
|
+
Next
|
|
380
|
+
</button>
|
|
381
|
+
</nav>
|
|
382
|
+
</>
|
|
383
|
+
)}
|
|
384
|
+
</section>
|
|
385
|
+
);
|
|
386
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react";
|
|
2
|
+
import type { SessionEvent, ToolActionReview } from "@opengeni/sdk";
|
|
3
|
+
import { approvalsFromRequiresAction } from "../approvals";
|
|
4
|
+
import { ToolActionReviewCard } from "./tool-action-review";
|
|
5
|
+
|
|
6
|
+
type HistoryContext = {
|
|
7
|
+
approvalIds: ReadonlySet<string>;
|
|
8
|
+
revision: number;
|
|
9
|
+
load: (approvalId: string) => Promise<ToolActionReview>;
|
|
10
|
+
onViewDetails: (review: ToolActionReview, path: string) => void;
|
|
11
|
+
};
|
|
12
|
+
const History = createContext<HistoryContext | null>(null);
|
|
13
|
+
|
|
14
|
+
/** Opt-in, authenticated history for the exact reviewed calls in the visible event window. */
|
|
15
|
+
export function ToolReviewHistoryProvider({
|
|
16
|
+
events,
|
|
17
|
+
load,
|
|
18
|
+
onViewDetails,
|
|
19
|
+
children,
|
|
20
|
+
}: {
|
|
21
|
+
events: readonly SessionEvent[];
|
|
22
|
+
load: HistoryContext["load"];
|
|
23
|
+
onViewDetails: HistoryContext["onViewDetails"];
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
const value = useMemo<HistoryContext>(
|
|
27
|
+
() => ({
|
|
28
|
+
approvalIds: new Set(
|
|
29
|
+
events.flatMap((event) =>
|
|
30
|
+
event.type === "session.requiresAction"
|
|
31
|
+
? approvalsFromRequiresAction(event.payload).map((approval) => approval.id)
|
|
32
|
+
: [],
|
|
33
|
+
),
|
|
34
|
+
),
|
|
35
|
+
revision:
|
|
36
|
+
[...events]
|
|
37
|
+
.reverse()
|
|
38
|
+
.find(
|
|
39
|
+
(event) =>
|
|
40
|
+
event.type === "session.requiresAction" ||
|
|
41
|
+
event.type === "agent.toolCall.output" ||
|
|
42
|
+
event.type.startsWith("turn.") ||
|
|
43
|
+
event.type === "user.approvalDecision" ||
|
|
44
|
+
event.type.startsWith("session.control."),
|
|
45
|
+
)?.sequence ?? 0,
|
|
46
|
+
load,
|
|
47
|
+
onViewDetails,
|
|
48
|
+
}),
|
|
49
|
+
[events, load, onViewDetails],
|
|
50
|
+
);
|
|
51
|
+
return <History.Provider value={value}>{children}</History.Provider>;
|
|
52
|
+
}
|
|
53
|
+
export function useHasToolReview(approvalId: string | null): boolean {
|
|
54
|
+
const history = useContext(History);
|
|
55
|
+
return Boolean(approvalId && history?.approvalIds.has(approvalId));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Recorded calls have details, never another decision button. */
|
|
59
|
+
export function ToolReviewHistoryReceipt({ approvalId }: { approvalId: string }) {
|
|
60
|
+
const history = useContext(History);
|
|
61
|
+
const [review, setReview] = useState<ToolActionReview | null>(null);
|
|
62
|
+
const [failed, setFailed] = useState(false);
|
|
63
|
+
const [retry, setRetry] = useState(0);
|
|
64
|
+
const load = history?.load;
|
|
65
|
+
const revision = history?.revision;
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
if (!load) return;
|
|
68
|
+
let active = true;
|
|
69
|
+
setFailed(false);
|
|
70
|
+
void load(approvalId).then(
|
|
71
|
+
(value) => {
|
|
72
|
+
if (active) {
|
|
73
|
+
if (value.id === approvalId) setReview(value);
|
|
74
|
+
else setFailed(true);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
() => {
|
|
78
|
+
if (active) setFailed(true);
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
return () => {
|
|
82
|
+
active = false;
|
|
83
|
+
};
|
|
84
|
+
}, [approvalId, load, revision, retry]);
|
|
85
|
+
if (!history) return null;
|
|
86
|
+
if (failed)
|
|
87
|
+
return (
|
|
88
|
+
<p className="text-og-sm text-og-fg-muted" role="alert">
|
|
89
|
+
Review details unavailable.{" "}
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
className="min-h-11 underline"
|
|
93
|
+
onClick={() => setRetry((value) => value + 1)}
|
|
94
|
+
>
|
|
95
|
+
Try again
|
|
96
|
+
</button>
|
|
97
|
+
</p>
|
|
98
|
+
);
|
|
99
|
+
if (!review)
|
|
100
|
+
return (
|
|
101
|
+
<p role="status" className="text-og-sm text-og-fg-muted">
|
|
102
|
+
Loading reviewed action…
|
|
103
|
+
</p>
|
|
104
|
+
);
|
|
105
|
+
return (
|
|
106
|
+
<div data-approval-id={approvalId} data-review-origin="history">
|
|
107
|
+
<ToolActionReviewCard
|
|
108
|
+
review={{ ...review, availableActions: [] }}
|
|
109
|
+
onViewDetails={(path) => history.onViewDetails(review, path)}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -328,6 +328,8 @@ export type {
|
|
|
328
328
|
export { approvalsFromRequiresAction, projectPendingApprovals } from "./approvals";
|
|
329
329
|
export type { PendingApproval } from "./approvals";
|
|
330
330
|
export { ApprovalSurface, defaultApprovalSurfaceMessages } from "./components/approval-surface";
|
|
331
|
+
export { ToolActionReviewCard, ToolActionReviewDetails } from "./components/tool-action-review";
|
|
332
|
+
export type { ToolReviewDetailsLoader } from "./components/tool-action-review";
|
|
331
333
|
export type { ApprovalSurfaceMessages, ApprovalSurfaceProps } from "./components/approval-surface";
|
|
332
334
|
|
|
333
335
|
// Structured human input: event projection, authoritative hook, and styled form.
|
|
@@ -491,7 +493,11 @@ export { SessionConversation } from "./components/session-conversation";
|
|
|
491
493
|
export { SessionList } from "./components/session-list";
|
|
492
494
|
export type { SessionListLabels, SessionListProps } from "./components/session-list";
|
|
493
495
|
export { OpenGeniChat } from "./components/open-geni-chat";
|
|
494
|
-
export type {
|
|
496
|
+
export type {
|
|
497
|
+
OpenGeniChatCreateOptions,
|
|
498
|
+
OpenGeniChatLabels,
|
|
499
|
+
OpenGeniChatProps,
|
|
500
|
+
} from "./components/open-geni-chat";
|
|
495
501
|
export type {
|
|
496
502
|
SessionConversationLabels,
|
|
497
503
|
SessionConversationProps,
|
|
@@ -714,3 +720,5 @@ export { ClaudeMark } from "./components/claude-mark";
|
|
|
714
720
|
export { AnthropicMark } from "./components/anthropic-mark";
|
|
715
721
|
export { OpenRouterMark } from "./components/openrouter-mark";
|
|
716
722
|
export { GrokMark } from "./components/grok-mark";
|
|
723
|
+
|
|
724
|
+
export { ToolReviewHistoryProvider } from "./components/tool-review-history";
|
package/src/session-ui.ts
CHANGED
|
@@ -9,6 +9,8 @@ export type {
|
|
|
9
9
|
} from "./components/human-input-form";
|
|
10
10
|
export { HumanInputSurface } from "./components/human-input-surface";
|
|
11
11
|
export { ApprovalSurface } from "./components/approval-surface";
|
|
12
|
+
export { ToolActionReviewCard, ToolActionReviewDetails } from "./components/tool-action-review";
|
|
13
|
+
export type { ToolReviewDetailsLoader } from "./components/tool-action-review";
|
|
12
14
|
export type { ApprovalSurfaceProps, ApprovalSurfaceMessages } from "./components/approval-surface";
|
|
13
15
|
export type { HumanInputSurfaceProps } from "./components/human-input-surface";
|
|
14
16
|
export { MessageTimeline, TimelineRow } from "./components/message-timeline";
|
|
@@ -69,7 +71,11 @@ export { SessionConversation } from "./components/session-conversation";
|
|
|
69
71
|
export { SessionList } from "./components/session-list";
|
|
70
72
|
export type { SessionListLabels, SessionListProps } from "./components/session-list";
|
|
71
73
|
export { OpenGeniChat } from "./components/open-geni-chat";
|
|
72
|
-
export type {
|
|
74
|
+
export type {
|
|
75
|
+
OpenGeniChatCreateOptions,
|
|
76
|
+
OpenGeniChatLabels,
|
|
77
|
+
OpenGeniChatProps,
|
|
78
|
+
} from "./components/open-geni-chat";
|
|
73
79
|
export type {
|
|
74
80
|
SessionConversationLabels,
|
|
75
81
|
SessionConversationProps,
|
|
@@ -95,3 +101,5 @@ export type { KnowledgeActivityActions } from "./timeline/knowledge-receipt";
|
|
|
95
101
|
export { StartupTimings } from "./timeline/startup-timings";
|
|
96
102
|
export { useStartupDetails, setStartupDetails } from "./timeline/startup-preference";
|
|
97
103
|
export type { GenieLoadingOptions } from "./timeline/genie-loading";
|
|
104
|
+
|
|
105
|
+
export { ToolReviewHistoryProvider } from "./components/tool-review-history";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { useHasToolReview, ToolReviewHistoryReceipt } from "../components/tool-review-history";
|
|
1
2
|
import { KnowledgeReceiptRow } from "./knowledge-receipt";
|
|
2
3
|
import { defaultUrlTransform } from "react-markdown";
|
|
3
4
|
import { isRetainedImageContentType, useRetainedImageObjectUrl } from "./retained-image";
|
|
@@ -2333,6 +2334,12 @@ function RunOnRenderer({ item }: ToolRendererProps) {
|
|
|
2333
2334
|
* JSON stays in the expandable body only.
|
|
2334
2335
|
*/
|
|
2335
2336
|
function GenericRenderer({ item }: ToolRendererProps) {
|
|
2337
|
+
const reviewed = useHasToolReview(item.callId);
|
|
2338
|
+
if (reviewed && item.callId) return <ToolReviewHistoryReceipt approvalId={item.callId} />;
|
|
2339
|
+
return <UnreviewedGenericRenderer item={item} />;
|
|
2340
|
+
}
|
|
2341
|
+
|
|
2342
|
+
function UnreviewedGenericRenderer({ item }: ToolRendererProps) {
|
|
2336
2343
|
const running = item.status === "running";
|
|
2337
2344
|
const args = parseToolArgs(item.arguments);
|
|
2338
2345
|
const display = toolDisplayName(item.name, item.display);
|
package/styles/compiled.css
CHANGED
|
@@ -1786,6 +1786,9 @@
|
|
|
1786
1786
|
:where(.og-root).mt-5,:where(.og-root) .mt-5 {
|
|
1787
1787
|
margin-top: calc(var(--spacing) * 5);
|
|
1788
1788
|
}
|
|
1789
|
+
:where(.og-root).mt-6,:where(.og-root) .mt-6 {
|
|
1790
|
+
margin-top: calc(var(--spacing) * 6);
|
|
1791
|
+
}
|
|
1789
1792
|
:where(.og-root).mt-auto,:where(.og-root) .mt-auto {
|
|
1790
1793
|
margin-top: auto;
|
|
1791
1794
|
}
|
|
@@ -1828,6 +1831,9 @@
|
|
|
1828
1831
|
:where(.og-root).mb-3,:where(.og-root) .mb-3 {
|
|
1829
1832
|
margin-bottom: calc(var(--spacing) * 3);
|
|
1830
1833
|
}
|
|
1834
|
+
:where(.og-root).mb-5,:where(.og-root) .mb-5 {
|
|
1835
|
+
margin-bottom: calc(var(--spacing) * 5);
|
|
1836
|
+
}
|
|
1831
1837
|
:where(.og-root).ml-0\.5,:where(.og-root) .ml-0\.5 {
|
|
1832
1838
|
margin-left: calc(var(--spacing) * 0.5);
|
|
1833
1839
|
}
|
|
@@ -2065,9 +2071,6 @@
|
|
|
2065
2071
|
:where(.og-root).max-h-40,:where(.og-root) .max-h-40 {
|
|
2066
2072
|
max-height: calc(var(--spacing) * 40);
|
|
2067
2073
|
}
|
|
2068
|
-
:where(.og-root).max-h-48,:where(.og-root) .max-h-48 {
|
|
2069
|
-
max-height: calc(var(--spacing) * 48);
|
|
2070
|
-
}
|
|
2071
2074
|
:where(.og-root).max-h-56,:where(.og-root) .max-h-56 {
|
|
2072
2075
|
max-height: calc(var(--spacing) * 56);
|
|
2073
2076
|
}
|
|
@@ -2554,8 +2557,8 @@
|
|
|
2554
2557
|
:where(.og-root).grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1fr\)\],:where(.og-root) .grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1fr\)\] {
|
|
2555
2558
|
grid-template-columns: minmax(0,1fr) minmax(0,1fr);
|
|
2556
2559
|
}
|
|
2557
|
-
:where(.og-root).grid-cols-\[minmax\(
|
|
2558
|
-
grid-template-columns: minmax(
|
|
2560
|
+
:where(.og-root).grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,2fr\)\],:where(.og-root) .grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,2fr\)\] {
|
|
2561
|
+
grid-template-columns: minmax(0,1fr) minmax(0,2fr);
|
|
2559
2562
|
}
|
|
2560
2563
|
:where(.og-root).flex-col,:where(.og-root) .flex-col {
|
|
2561
2564
|
flex-direction: column;
|
|
@@ -2731,14 +2734,6 @@
|
|
|
2731
2734
|
}
|
|
2732
2735
|
}
|
|
2733
2736
|
}
|
|
2734
|
-
:where(.og-root).divide-og-status-waiting\/20,:where(.og-root) .divide-og-status-waiting\/20 {
|
|
2735
|
-
:where(& > :not(:last-child)) {
|
|
2736
|
-
border-color: var(--og-color-status-waiting);
|
|
2737
|
-
@supports (color: color-mix(in lab, red, red)) {
|
|
2738
|
-
border-color: color-mix(in oklab, var(--og-color-status-waiting) 20%, transparent);
|
|
2739
|
-
}
|
|
2740
|
-
}
|
|
2741
|
-
}
|
|
2742
2737
|
:where(.og-root).self-center,:where(.og-root) .self-center {
|
|
2743
2738
|
align-self: center;
|
|
2744
2739
|
}
|
|
@@ -3744,6 +3739,9 @@
|
|
|
3744
3739
|
:where(.og-root).pt-3,:where(.og-root) .pt-3 {
|
|
3745
3740
|
padding-top: calc(var(--spacing) * 3);
|
|
3746
3741
|
}
|
|
3742
|
+
:where(.og-root).pt-4,:where(.og-root) .pt-4 {
|
|
3743
|
+
padding-top: calc(var(--spacing) * 4);
|
|
3744
|
+
}
|
|
3747
3745
|
:where(.og-root).pt-16,:where(.og-root) .pt-16 {
|
|
3748
3746
|
padding-top: calc(var(--spacing) * 16);
|
|
3749
3747
|
}
|
|
@@ -5055,6 +5053,16 @@
|
|
|
5055
5053
|
}
|
|
5056
5054
|
}
|
|
5057
5055
|
}
|
|
5056
|
+
:where(.og-root).hover\:bg-og-fg\/5,:where(.og-root) .hover\:bg-og-fg\/5 {
|
|
5057
|
+
&:hover {
|
|
5058
|
+
@media (hover: hover) {
|
|
5059
|
+
background-color: var(--og-color-fg);
|
|
5060
|
+
@supports (color: color-mix(in lab, red, red)) {
|
|
5061
|
+
background-color: color-mix(in oklab, var(--og-color-fg) 5%, transparent);
|
|
5062
|
+
}
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5058
5066
|
:where(.og-root).hover\:bg-og-hover,:where(.og-root) .hover\:bg-og-hover {
|
|
5059
5067
|
&:hover {
|
|
5060
5068
|
@media (hover: hover) {
|
|
@@ -5288,6 +5296,13 @@
|
|
|
5288
5296
|
}
|
|
5289
5297
|
}
|
|
5290
5298
|
}
|
|
5299
|
+
:where(.og-root).hover\:decoration-current,:where(.og-root) .hover\:decoration-current {
|
|
5300
|
+
&:hover {
|
|
5301
|
+
@media (hover: hover) {
|
|
5302
|
+
text-decoration-color: currentcolor;
|
|
5303
|
+
}
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5291
5306
|
:where(.og-root).hover\:opacity-80,:where(.og-root) .hover\:opacity-80 {
|
|
5292
5307
|
&:hover {
|
|
5293
5308
|
@media (hover: hover) {
|
|
@@ -6525,6 +6540,11 @@
|
|
|
6525
6540
|
margin-block: calc(var(--spacing) * 1);
|
|
6526
6541
|
}
|
|
6527
6542
|
}
|
|
6543
|
+
:where(.og-root).\[\@media\(pointer\:coarse\)\]\:min-h-11,:where(.og-root) .\[\@media\(pointer\:coarse\)\]\:min-h-11 {
|
|
6544
|
+
@media (pointer:coarse) {
|
|
6545
|
+
min-height: calc(var(--spacing) * 11);
|
|
6546
|
+
}
|
|
6547
|
+
}
|
|
6528
6548
|
:where(.og-root).\[tr\:last-child\>\&\]\:border-b-0,:where(.og-root) .\[tr\:last-child\>\&\]\:border-b-0 {
|
|
6529
6549
|
tr:last-child>& {
|
|
6530
6550
|
border-bottom-style: var(--tw-border-style);
|