@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
|
@@ -1,39 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import {
|
|
3
|
+
OpenGeniApiError,
|
|
4
|
+
toolReviewAction,
|
|
5
|
+
toolReviewFields,
|
|
6
|
+
toolReviewDetails,
|
|
7
|
+
type ToolActionReview,
|
|
8
|
+
} from "@opengeni/sdk";
|
|
3
9
|
import type { PendingApproval } from "../approvals";
|
|
4
10
|
import { cn } from "../lib/cn";
|
|
5
11
|
import { useErrorMessage } from "../lib/error-message";
|
|
6
12
|
import { toolDisplayName } from "../timeline/tool-display-name";
|
|
13
|
+
import {
|
|
14
|
+
ToolActionReviewCard,
|
|
15
|
+
ToolActionReviewDetails,
|
|
16
|
+
type ToolReviewDetailsLoader,
|
|
17
|
+
} from "./tool-action-review";
|
|
7
18
|
|
|
8
19
|
export type ApprovalSurfaceMessages = {
|
|
9
20
|
title: string;
|
|
10
|
-
/** Screen-reader context for the decision; not shown visually. */
|
|
11
21
|
description: string;
|
|
12
22
|
approve: string;
|
|
13
23
|
reject: string;
|
|
14
24
|
approving: string;
|
|
15
25
|
rejecting: string;
|
|
16
26
|
formatToolName: (name: string) => string;
|
|
17
|
-
/** Toggle for the exact arguments when they are shown as readable fields. */
|
|
18
27
|
showDetails?: string | undefined;
|
|
19
28
|
hideDetails?: string | undefined;
|
|
20
29
|
};
|
|
21
|
-
|
|
22
30
|
export const defaultApprovalSurfaceMessages: ApprovalSurfaceMessages = {
|
|
23
31
|
title: "Approval needed",
|
|
24
|
-
description: "Review the
|
|
25
|
-
approve: "Approve",
|
|
26
|
-
reject: "
|
|
32
|
+
description: "Review the action before it runs.",
|
|
33
|
+
approve: "Approve action",
|
|
34
|
+
reject: "Decline",
|
|
27
35
|
approving: "Approving…",
|
|
28
|
-
rejecting: "
|
|
36
|
+
rejecting: "Declining…",
|
|
29
37
|
formatToolName: (name) =>
|
|
30
38
|
name.includes("__") || /^[a-f0-9]{64}$/.test(name)
|
|
31
39
|
? toolDisplayName(name)
|
|
32
40
|
: name.replaceAll("_", " ").replaceAll(".", " › "),
|
|
33
|
-
showDetails: "
|
|
34
|
-
hideDetails: "
|
|
41
|
+
showDetails: "View full details",
|
|
42
|
+
hideDetails: "Back to review",
|
|
35
43
|
};
|
|
36
|
-
|
|
37
44
|
export type ApprovalSurfaceProps = {
|
|
38
45
|
approvals: PendingApproval[];
|
|
39
46
|
onApprove: (approval: PendingApproval) => void | Promise<void>;
|
|
@@ -42,291 +49,302 @@ export type ApprovalSurfaceProps = {
|
|
|
42
49
|
error?: string | Error | null | undefined;
|
|
43
50
|
messages?: Partial<ApprovalSurfaceMessages> | undefined;
|
|
44
51
|
renderApproval?: ((approval: PendingApproval) => ReactNode) | undefined;
|
|
52
|
+
/** Authenticated server facts; hosts need not import any web-app code. */
|
|
53
|
+
loadReview?: ((approval: PendingApproval) => Promise<ToolActionReview>) | undefined;
|
|
54
|
+
loadDetails?: ToolReviewDetailsLoader | undefined;
|
|
55
|
+
onViewDetails?: ((review: ToolActionReview, path: string) => void) | undefined;
|
|
56
|
+
selectedApprovalId?: string | null | undefined;
|
|
57
|
+
onSelectedApprovalChange?: ((id: string) => void) | undefined;
|
|
45
58
|
className?: string | undefined;
|
|
46
59
|
};
|
|
47
|
-
|
|
48
|
-
const APPROVAL_ARGUMENT_PREVIEW_CHARACTERS = 4_000;
|
|
49
|
-
const APPROVAL_FIELD_LIMIT = 12;
|
|
50
|
-
const APPROVAL_LIST_LIMIT = 10;
|
|
51
|
-
|
|
52
|
-
function approvalArgumentsPreview(value: unknown): string | null {
|
|
53
|
-
if (value === undefined) return null;
|
|
54
|
-
let serialized: string;
|
|
55
|
-
try {
|
|
56
|
-
serialized =
|
|
57
|
-
typeof value === "string" ? value : (JSON.stringify(value, null, 2) ?? String(value));
|
|
58
|
-
} catch {
|
|
59
|
-
serialized = "[Arguments unavailable]";
|
|
60
|
-
}
|
|
61
|
-
const characters = Array.from(serialized);
|
|
62
|
-
if (characters.length <= APPROVAL_ARGUMENT_PREVIEW_CHARACTERS) return serialized;
|
|
63
|
-
return `${characters.slice(0, APPROVAL_ARGUMENT_PREVIEW_CHARACTERS).join("")}\n… ${characters.length - APPROVAL_ARGUMENT_PREVIEW_CHARACTERS} characters omitted`;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
60
|
export type ApprovalField = { key: string; label: string; value: string };
|
|
67
|
-
|
|
68
|
-
const ACRONYMS = new Set(["id", "ids", "url", "uri", "api", "ip", "sku", "utc", "iban", "vat"]);
|
|
69
|
-
|
|
70
|
-
/** "paymentId" / "payment_id" → "Payment ID". */
|
|
71
|
-
function fieldLabel(key: string): string {
|
|
72
|
-
const words = key
|
|
73
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
74
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
75
|
-
.split(/[\s_.-]+/)
|
|
76
|
-
.filter(Boolean)
|
|
77
|
-
.map((word) => (ACRONYMS.has(word.toLowerCase()) ? word.toUpperCase() : word.toLowerCase()));
|
|
78
|
-
const label = words.join(" ");
|
|
79
|
-
return label ? label.charAt(0).toUpperCase() + label.slice(1) : key;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function scalarText(value: unknown): string | null {
|
|
83
|
-
if (typeof value === "string") return value;
|
|
84
|
-
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
85
|
-
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
86
|
-
if (value === null) return "None";
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Readable fields for a flat argument object (scalars and short scalar lists).
|
|
92
|
-
* Returns null when the arguments are nested, large, or not an object, so the
|
|
93
|
-
* caller shows the exact JSON instead of a lossy summary.
|
|
94
|
-
*/
|
|
61
|
+
/** @deprecated Prefer the versioned server review; retained for small custom renderers. */
|
|
95
62
|
export function approvalArgumentFields(value: unknown): ApprovalField[] | null {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
if (!record || typeof record !== "object" || Array.isArray(record)) return null;
|
|
105
|
-
const entries = Object.entries(record as Record<string, unknown>);
|
|
106
|
-
if (entries.length === 0 || entries.length > APPROVAL_FIELD_LIMIT) return null;
|
|
107
|
-
const fields: ApprovalField[] = [];
|
|
108
|
-
for (const [key, entry] of entries) {
|
|
109
|
-
let text = scalarText(entry);
|
|
110
|
-
if (text === null && Array.isArray(entry) && entry.length <= APPROVAL_LIST_LIMIT) {
|
|
111
|
-
const items = entry.map(scalarText);
|
|
112
|
-
if (items.every((item): item is string => item !== null)) text = items.join(", ");
|
|
113
|
-
}
|
|
114
|
-
if (text === null || text.length > APPROVAL_ARGUMENT_PREVIEW_CHARACTERS) return null;
|
|
115
|
-
fields.push({ key, label: fieldLabel(key), value: text });
|
|
116
|
-
}
|
|
117
|
-
return fields;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function isEmptyArguments(value: unknown): boolean {
|
|
121
|
-
return (
|
|
122
|
-
value === undefined ||
|
|
123
|
-
value === null ||
|
|
124
|
-
(typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0)
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function approvalOwnershipKey(approval: PendingApproval): string {
|
|
129
|
-
return `${approval.id}\u0000${approval.name}\u0000${approvalArgumentsPreview(approval.arguments) ?? ""}`;
|
|
63
|
+
const projected = toolReviewFields(value);
|
|
64
|
+
return projected.fields.length
|
|
65
|
+
? projected.fields.map((field) => ({
|
|
66
|
+
key: field.path.slice(1),
|
|
67
|
+
label: field.label,
|
|
68
|
+
value: field.preview,
|
|
69
|
+
}))
|
|
70
|
+
: null;
|
|
130
71
|
}
|
|
131
72
|
|
|
132
|
-
/**
|
|
133
|
-
* Host-neutral approval presentation backed by the native pending-approval
|
|
134
|
-
* projection and control callbacks. It owns only presentation and duplicate
|
|
135
|
-
* click fencing; OpenGeni remains the authority for approval state.
|
|
136
|
-
*/
|
|
73
|
+
/** Hosts provide decisions; this surface never mutates the approved payload. */
|
|
137
74
|
export function ApprovalSurface({
|
|
138
75
|
approvals,
|
|
139
76
|
onApprove,
|
|
140
77
|
onReject,
|
|
141
|
-
responding
|
|
78
|
+
responding,
|
|
142
79
|
error,
|
|
143
80
|
messages: overrides,
|
|
144
81
|
renderApproval,
|
|
82
|
+
loadReview,
|
|
83
|
+
loadDetails,
|
|
84
|
+
onViewDetails,
|
|
85
|
+
selectedApprovalId,
|
|
86
|
+
onSelectedApprovalChange,
|
|
145
87
|
className,
|
|
146
88
|
}: ApprovalSurfaceProps) {
|
|
147
|
-
const titleId = useId();
|
|
148
|
-
const formatError = useErrorMessage();
|
|
149
|
-
const descriptionId = useId();
|
|
150
89
|
const messages = { ...defaultApprovalSurfaceMessages, ...overrides };
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const pendingRef = useRef<{ approvalKey: string; token: symbol } | null>(null);
|
|
90
|
+
const formatError = useErrorMessage();
|
|
91
|
+
const [pending, setPending] = useState<{ id: string; decision: "approve" | "reject" } | null>(
|
|
92
|
+
null,
|
|
93
|
+
);
|
|
94
|
+
const pendingRef = useRef<string | null>(null);
|
|
157
95
|
const [decisionError, setDecisionError] = useState<{ cause: unknown } | null>(null);
|
|
158
|
-
|
|
96
|
+
const [localSelectedId, setLocalSelectedId] = useState<string | null>(null);
|
|
97
|
+
const selectedId = selectedApprovalId === undefined ? localSelectedId : selectedApprovalId;
|
|
98
|
+
const setSelectedId = (id: string) => {
|
|
99
|
+
setLocalSelectedId(id);
|
|
100
|
+
onSelectedApprovalChange?.(id);
|
|
101
|
+
};
|
|
102
|
+
const surface = useRef<HTMLElement>(null);
|
|
159
103
|
useEffect(() => {
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (pendingRef.current?.token === pending.token) {
|
|
165
|
-
pendingRef.current = null;
|
|
166
|
-
}
|
|
167
|
-
setPending((current) => (current?.token === pending.token ? null : current));
|
|
104
|
+
if (pending && !approvals.some((approval) => approval.id === pending.id)) {
|
|
105
|
+
pendingRef.current = null;
|
|
106
|
+
setPending(null);
|
|
107
|
+
requestAnimationFrame(() => surface.current?.querySelector<HTMLElement>("h3")?.focus());
|
|
168
108
|
}
|
|
169
109
|
}, [approvals, pending]);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (responding || pendingRef.current
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
pendingRef.current = { approvalKey, token };
|
|
181
|
-
setPending({ approvalKey, decision, token });
|
|
110
|
+
if (!approvals.length) return null;
|
|
111
|
+
const selectedIndex = Math.max(
|
|
112
|
+
0,
|
|
113
|
+
approvals.findIndex((approval) => approval.id === selectedId),
|
|
114
|
+
);
|
|
115
|
+
const selected = approvals[selectedIndex]!;
|
|
116
|
+
const decide = async (approval: PendingApproval, decision: "approve" | "reject") => {
|
|
117
|
+
if (responding || pendingRef.current) return;
|
|
118
|
+
pendingRef.current = approval.id;
|
|
119
|
+
setPending({ id: approval.id, decision });
|
|
182
120
|
setDecisionError(null);
|
|
183
121
|
try {
|
|
184
122
|
await (decision === "approve" ? onApprove(approval) : onReject(approval));
|
|
185
|
-
// Keep the decision fenced until the authoritative projection removes or
|
|
186
|
-
// replaces this exact approval. A successful callback is transport
|
|
187
|
-
// acceptance, not settlement.
|
|
188
123
|
} catch (cause) {
|
|
189
|
-
if (pendingRef.current
|
|
124
|
+
if (pendingRef.current === approval.id) {
|
|
190
125
|
pendingRef.current = null;
|
|
191
|
-
setPending(
|
|
126
|
+
setPending(null);
|
|
192
127
|
setDecisionError({ cause });
|
|
193
128
|
}
|
|
194
129
|
}
|
|
195
130
|
};
|
|
196
|
-
const
|
|
131
|
+
const errorText = decisionError
|
|
197
132
|
? formatError(decisionError.cause)
|
|
198
133
|
: error instanceof Error
|
|
199
134
|
? formatError(error)
|
|
200
135
|
: error;
|
|
201
|
-
|
|
202
136
|
return (
|
|
203
137
|
<section
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
)}
|
|
208
|
-
aria-labelledby={titleId}
|
|
209
|
-
aria-describedby={descriptionId}
|
|
138
|
+
ref={surface}
|
|
139
|
+
aria-label={messages.title}
|
|
140
|
+
className={cn("og-root box-border w-full min-w-0 border-t border-og-border", className)}
|
|
210
141
|
data-og-approval-surface=""
|
|
211
142
|
>
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
key={approval.id}
|
|
225
|
-
data-approval-id={approval.id}
|
|
226
|
-
className="flex flex-col gap-3 p-4"
|
|
143
|
+
{approvals.length > 1 && (
|
|
144
|
+
<nav
|
|
145
|
+
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"
|
|
147
|
+
>
|
|
148
|
+
<span>{approvals.length} actions need review</span>
|
|
149
|
+
<div className="flex items-center gap-3">
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
disabled={selectedIndex === 0}
|
|
153
|
+
onClick={() => setSelectedId(approvals[selectedIndex - 1]!.id)}
|
|
154
|
+
className="min-h-11 disabled:opacity-40"
|
|
227
155
|
>
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
})}
|
|
156
|
+
Previous
|
|
157
|
+
</button>
|
|
158
|
+
<span>
|
|
159
|
+
{selectedIndex + 1} of {approvals.length}
|
|
160
|
+
</span>
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
disabled={selectedIndex === approvals.length - 1}
|
|
164
|
+
onClick={() => setSelectedId(approvals[selectedIndex + 1]!.id)}
|
|
165
|
+
className="min-h-11 disabled:opacity-40"
|
|
166
|
+
>
|
|
167
|
+
Next
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
</nav>
|
|
171
|
+
)}
|
|
172
|
+
<div className="divide-y divide-og-border">
|
|
173
|
+
{[selected].map((approval) => (
|
|
174
|
+
<ApprovalRequest
|
|
175
|
+
key={approval.id}
|
|
176
|
+
approval={approval}
|
|
177
|
+
loadReview={loadReview}
|
|
178
|
+
loadDetails={loadDetails}
|
|
179
|
+
messages={messages}
|
|
180
|
+
custom={renderApproval?.(approval)}
|
|
181
|
+
onViewDetails={onViewDetails}
|
|
182
|
+
submitting={pending?.id === approval.id ? pending.decision : null}
|
|
183
|
+
disabled={Boolean(responding || pending)}
|
|
184
|
+
onApprove={() => void decide(approval, "approve")}
|
|
185
|
+
onReject={() => void decide(approval, "reject")}
|
|
186
|
+
/>
|
|
187
|
+
))}
|
|
261
188
|
</div>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
{errorMessage}
|
|
189
|
+
{errorText && (
|
|
190
|
+
<p role="alert" className="m-0 pb-3 text-og-sm text-og-status-failed">
|
|
191
|
+
{errorText}
|
|
266
192
|
</p>
|
|
267
|
-
)
|
|
193
|
+
)}
|
|
268
194
|
</section>
|
|
269
195
|
);
|
|
270
196
|
}
|
|
271
197
|
|
|
272
|
-
function
|
|
198
|
+
function ApprovalRequest({
|
|
273
199
|
approval,
|
|
200
|
+
loadReview,
|
|
201
|
+
loadDetails,
|
|
274
202
|
messages,
|
|
203
|
+
custom,
|
|
204
|
+
onViewDetails,
|
|
205
|
+
...actions
|
|
275
206
|
}: {
|
|
276
207
|
approval: PendingApproval;
|
|
208
|
+
loadReview: ApprovalSurfaceProps["loadReview"];
|
|
277
209
|
messages: ApprovalSurfaceMessages;
|
|
210
|
+
loadDetails: ToolReviewDetailsLoader | undefined;
|
|
211
|
+
custom: ReactNode;
|
|
212
|
+
onViewDetails: ApprovalSurfaceProps["onViewDetails"];
|
|
213
|
+
submitting: "approve" | "reject" | null;
|
|
214
|
+
disabled: boolean;
|
|
215
|
+
onApprove: () => void;
|
|
216
|
+
onReject: () => void;
|
|
278
217
|
}) {
|
|
279
|
-
const [
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
218
|
+
const [loaded, setLoaded] = useState<ToolActionReview | null>(null);
|
|
219
|
+
const [failed, setFailed] = useState(false);
|
|
220
|
+
const [legacy, setLegacy] = useState(false);
|
|
221
|
+
const [retry, setRetry] = useState(0);
|
|
222
|
+
const [detailPath, setDetailPath] = useState<string | null>(null);
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
let active = true;
|
|
225
|
+
setLoaded(null);
|
|
226
|
+
setFailed(false);
|
|
227
|
+
setLegacy(false);
|
|
228
|
+
if (loadReview)
|
|
229
|
+
void loadReview(approval).then(
|
|
230
|
+
(review) => {
|
|
231
|
+
if (active) {
|
|
232
|
+
if (review.id !== approval.id) setFailed(true);
|
|
233
|
+
else setLoaded(review);
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
(error) => {
|
|
237
|
+
if (active) {
|
|
238
|
+
if (
|
|
239
|
+
error instanceof OpenGeniApiError &&
|
|
240
|
+
error.status === 404 &&
|
|
241
|
+
approval.arguments !== undefined
|
|
242
|
+
)
|
|
243
|
+
setLegacy(true);
|
|
244
|
+
else setFailed(true);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
return () => {
|
|
249
|
+
active = false;
|
|
250
|
+
};
|
|
251
|
+
}, [approval, retry, loadReview]);
|
|
252
|
+
if (loadReview && !loaded && !failed && !legacy)
|
|
253
|
+
return (
|
|
254
|
+
<div className="py-5 text-og-sm text-og-fg-muted" role="status">
|
|
255
|
+
Loading action details…
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
if (failed)
|
|
259
|
+
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)}
|
|
266
|
+
>
|
|
267
|
+
Try again
|
|
268
|
+
</button>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
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
|
+
const review = loaded ?? fallback;
|
|
293
|
+
if (detailPath !== null)
|
|
294
|
+
return (
|
|
295
|
+
<ToolActionReviewDetails
|
|
296
|
+
review={review}
|
|
297
|
+
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
|
+
}
|
|
308
|
+
onBack={() => {
|
|
309
|
+
const path = detailPath;
|
|
310
|
+
setDetailPath(null);
|
|
311
|
+
requestAnimationFrame(() => {
|
|
312
|
+
[
|
|
313
|
+
...document.querySelectorAll<HTMLButtonElement>(
|
|
314
|
+
"[data-approval-id] button[data-review-path]",
|
|
315
|
+
),
|
|
316
|
+
]
|
|
317
|
+
.find(
|
|
318
|
+
(button) =>
|
|
319
|
+
button.closest("[data-approval-id]")?.getAttribute("data-approval-id") ===
|
|
320
|
+
approval.id && button.dataset.reviewPath === path,
|
|
321
|
+
)
|
|
322
|
+
?.focus();
|
|
323
|
+
});
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
);
|
|
284
327
|
return (
|
|
285
|
-
<div
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
328
|
+
<div data-approval-id={approval.id} data-review-origin="pending">
|
|
329
|
+
{custom}
|
|
330
|
+
<ToolActionReviewCard
|
|
331
|
+
review={review}
|
|
332
|
+
{...actions}
|
|
333
|
+
pendingLabel={messages.title}
|
|
334
|
+
rejectLabel={messages.reject}
|
|
335
|
+
onViewDetails={
|
|
336
|
+
loaded && onViewDetails
|
|
337
|
+
? (path) => onViewDetails(review, path)
|
|
338
|
+
: loadDetails || !loaded
|
|
339
|
+
? setDetailPath
|
|
340
|
+
: undefined
|
|
341
|
+
}
|
|
342
|
+
/>
|
|
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.
|
|
294
346
|
</p>
|
|
295
|
-
|
|
296
|
-
<p className="mt-0.5 break-words text-og-xs text-og-fg-muted">
|
|
297
|
-
{approval.display.accountLabel}
|
|
298
|
-
</p>
|
|
299
|
-
) : null}
|
|
300
|
-
{fields ? (
|
|
301
|
-
<dl className="m-0 mt-3 grid grid-cols-[minmax(5.5rem,max-content)_minmax(0,1fr)] gap-x-5 gap-y-1.5 text-og-sm">
|
|
302
|
-
{fields.map((field) => (
|
|
303
|
-
<Fragment key={field.key}>
|
|
304
|
-
<dt className="m-0 text-og-fg-muted">{field.label}</dt>
|
|
305
|
-
<dd className="m-0 max-h-32 min-w-0 overflow-auto whitespace-pre-wrap break-words text-og-fg">
|
|
306
|
-
{field.value}
|
|
307
|
-
</dd>
|
|
308
|
-
</Fragment>
|
|
309
|
-
))}
|
|
310
|
-
</dl>
|
|
311
|
-
) : null}
|
|
312
|
-
{exact !== null && (!fields || showExact) ? (
|
|
313
|
-
<pre className="mt-3 max-h-48 overflow-auto whitespace-pre-wrap break-words rounded-og-sm bg-og-fg/5 p-2.5 font-mono text-og-xs text-og-fg-muted">
|
|
314
|
-
{exact}
|
|
315
|
-
</pre>
|
|
316
|
-
) : null}
|
|
317
|
-
{fields && exact !== null ? (
|
|
318
|
-
<button
|
|
319
|
-
type="button"
|
|
320
|
-
aria-expanded={showExact}
|
|
321
|
-
onClick={() => setShowExact((value) => !value)}
|
|
322
|
-
className="mt-2 text-og-xs text-og-fg-muted underline-offset-2 hover:text-og-fg hover:underline"
|
|
323
|
-
>
|
|
324
|
-
{showExact
|
|
325
|
-
? (messages.hideDetails ?? defaultApprovalSurfaceMessages.hideDetails)
|
|
326
|
-
: (messages.showDetails ?? defaultApprovalSurfaceMessages.showDetails)}
|
|
327
|
-
</button>
|
|
328
|
-
) : null}
|
|
329
|
-
</div>
|
|
347
|
+
)}
|
|
330
348
|
</div>
|
|
331
349
|
);
|
|
332
350
|
}
|