assistant-ui 0.0.119 → 0.0.121
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 +60 -1
- package/dist/commands/cloud.d.ts +25 -0
- package/dist/commands/cloud.d.ts.map +1 -0
- package/dist/commands/cloud.js +194 -0
- package/dist/commands/cloud.js.map +1 -0
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +8 -0
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/info.js +1 -1
- package/dist/lib/agent-skill.d.ts +2 -2
- package/dist/lib/agent-skill.js +1 -1
- package/dist/lib/agent-skill.js.map +1 -1
- package/dist/lib/cloud-abort.d.ts +2 -0
- package/dist/lib/cloud-abort.d.ts.map +1 -0
- package/dist/lib/cloud-abort.js +13 -0
- package/dist/lib/cloud-abort.js.map +1 -0
- package/dist/lib/cloud-auth.d.ts +28 -0
- package/dist/lib/cloud-auth.d.ts.map +1 -0
- package/dist/lib/cloud-auth.js +169 -0
- package/dist/lib/cloud-auth.js.map +1 -0
- package/dist/lib/cloud-config.d.ts +21 -0
- package/dist/lib/cloud-config.d.ts.map +1 -0
- package/dist/lib/cloud-config.js +76 -0
- package/dist/lib/cloud-config.js.map +1 -0
- package/dist/lib/cloud-provision.d.ts +20 -0
- package/dist/lib/cloud-provision.d.ts.map +1 -0
- package/dist/lib/cloud-provision.js +55 -0
- package/dist/lib/cloud-provision.js.map +1 -0
- package/dist/lib/cloud-setup-login.d.ts +9 -0
- package/dist/lib/cloud-setup-login.d.ts.map +1 -0
- package/dist/lib/cloud-setup-login.js +137 -0
- package/dist/lib/cloud-setup-login.js.map +1 -0
- package/dist/lib/cloud-url.d.ts +2 -0
- package/dist/lib/cloud-url.d.ts.map +1 -0
- package/dist/lib/cloud-url.js +14 -0
- package/dist/lib/cloud-url.js.map +1 -0
- package/dist/lib/transform.js +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +2 -1
- package/dist/program.js.map +1 -1
- package/package.json +17 -13
- package/src/commands/cloud.test.ts +238 -0
- package/src/commands/cloud.ts +455 -0
- package/src/commands/create.ts +8 -0
- package/src/lib/agent-skill.ts +1 -1
- package/src/lib/cloud-abort.ts +12 -0
- package/src/lib/cloud-auth.test.ts +500 -0
- package/src/lib/cloud-auth.ts +273 -0
- package/src/lib/cloud-config.test.ts +82 -0
- package/src/lib/cloud-config.ts +138 -0
- package/src/lib/cloud-provision.test.ts +132 -0
- package/src/lib/cloud-provision.ts +93 -0
- package/src/lib/cloud-setup-login.test.ts +361 -0
- package/src/lib/cloud-setup-login.ts +240 -0
- package/src/lib/cloud-url.ts +19 -0
- package/src/program.ts +2 -0
- package/templates/cloud-harness/.assistant-ui/template.json +3 -0
- package/templates/cloud-harness/.env.example +1 -0
- package/templates/cloud-harness/.gitignore +42 -0
- package/templates/cloud-harness/README.md +28 -0
- package/templates/cloud-harness/app/api/chat/route.ts +13 -0
- package/templates/cloud-harness/app/api/credential/route.ts +14 -0
- package/templates/cloud-harness/app/assistant.tsx +83 -0
- package/templates/cloud-harness/app/favicon.ico +0 -0
- package/templates/cloud-harness/app/globals.css +139 -0
- package/templates/cloud-harness/app/layout.tsx +37 -0
- package/templates/cloud-harness/app/page.tsx +5 -0
- package/templates/cloud-harness/components/assistant-ui/elements/attachment.aui.tsx +279 -0
- package/templates/cloud-harness/components/assistant-ui/elements/markdown-text.tsx +238 -0
- package/templates/cloud-harness/components/assistant-ui/elements/thread.aui.tsx +521 -0
- package/templates/cloud-harness/components/assistant-ui/elements/tool-fallback.aui.tsx +1153 -0
- package/templates/cloud-harness/components/assistant-ui/elements/tooltip-icon-button.tsx +49 -0
- package/templates/cloud-harness/components/ui/avatar.tsx +109 -0
- package/templates/cloud-harness/components/ui/button.tsx +58 -0
- package/templates/cloud-harness/components/ui/collapsible.tsx +21 -0
- package/templates/cloud-harness/components/ui/dialog.tsx +159 -0
- package/templates/cloud-harness/components/ui/textarea.tsx +18 -0
- package/templates/cloud-harness/components/ui/tooltip.tsx +69 -0
- package/templates/cloud-harness/components.json +24 -0
- package/templates/cloud-harness/hooks/use-attachment-src.ts +46 -0
- package/templates/cloud-harness/hooks/use-copy-to-clipboard.ts +54 -0
- package/templates/cloud-harness/lib/utils.ts +1 -0
- package/templates/cloud-harness/next.config.ts +6 -0
- package/templates/cloud-harness/package.json +51 -0
- package/templates/cloud-harness/postcss.config.mjs +5 -0
- package/templates/cloud-harness/tsconfig.json +33 -0
- package/templates/cloud-harness/vendor/README.md +13 -0
- package/templates/cloud-harness/vendor/assistant-ui-react-harness-sdk-0.0.2.tgz +0 -0
- package/templates/cloud-harness/vendor/harness-sdk-0.3.1.tgz +0 -0
- package/templates/cloud-harness/vendor/provenance.json +38 -0
- package/templates/cloud-harness/vendor/statewire-0.19.3.tgz +0 -0
- /package/{LICENSE → templates/cloud-harness/vendor/LICENSE} +0 -0
|
@@ -0,0 +1,1153 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { memo, useCallback, useRef, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
AlertCircleIcon,
|
|
6
|
+
CheckIcon,
|
|
7
|
+
ChevronDownIcon,
|
|
8
|
+
CircleMinusIcon,
|
|
9
|
+
LoaderIcon,
|
|
10
|
+
XCircleIcon,
|
|
11
|
+
} from "lucide-react";
|
|
12
|
+
import {
|
|
13
|
+
toolApprovalAcceptsText,
|
|
14
|
+
useAuiState,
|
|
15
|
+
useScrollLock,
|
|
16
|
+
useToolCallElapsed,
|
|
17
|
+
type ToolApprovalAnswer,
|
|
18
|
+
type ToolApprovalOption,
|
|
19
|
+
type ToolApprovalQuestion,
|
|
20
|
+
type ToolCallMessagePart,
|
|
21
|
+
type ToolCallMessagePartProps,
|
|
22
|
+
type ToolCallMessagePartStatus,
|
|
23
|
+
type ToolCallMessagePartComponent,
|
|
24
|
+
} from "@assistant-ui/react";
|
|
25
|
+
import {
|
|
26
|
+
Collapsible,
|
|
27
|
+
CollapsibleContent,
|
|
28
|
+
CollapsibleTrigger,
|
|
29
|
+
} from "@/components/ui/collapsible";
|
|
30
|
+
import { cn } from "@/lib/utils";
|
|
31
|
+
import { Button } from "@/components/ui/button";
|
|
32
|
+
import { Textarea } from "@/components/ui/textarea";
|
|
33
|
+
|
|
34
|
+
const ANIMATION_DURATION = 200;
|
|
35
|
+
|
|
36
|
+
const pressable = "active:scale-[0.98]";
|
|
37
|
+
|
|
38
|
+
export type ToolFallbackRootProps = Omit<
|
|
39
|
+
React.ComponentProps<typeof Collapsible>,
|
|
40
|
+
"open" | "onOpenChange"
|
|
41
|
+
> & {
|
|
42
|
+
open?: boolean;
|
|
43
|
+
onOpenChange?: (open: boolean) => void;
|
|
44
|
+
defaultOpen?: boolean;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function ToolFallbackRoot({
|
|
48
|
+
className,
|
|
49
|
+
open: controlledOpen,
|
|
50
|
+
onOpenChange: controlledOnOpenChange,
|
|
51
|
+
defaultOpen = false,
|
|
52
|
+
children,
|
|
53
|
+
...props
|
|
54
|
+
}: ToolFallbackRootProps) {
|
|
55
|
+
const collapsibleRef = useRef<HTMLDivElement>(null);
|
|
56
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
57
|
+
const lockScroll = useScrollLock(collapsibleRef, ANIMATION_DURATION);
|
|
58
|
+
|
|
59
|
+
const isControlled = controlledOpen !== undefined;
|
|
60
|
+
const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
|
|
61
|
+
|
|
62
|
+
const handleOpenChange = useCallback(
|
|
63
|
+
(open: boolean) => {
|
|
64
|
+
lockScroll();
|
|
65
|
+
if (!isControlled) {
|
|
66
|
+
setUncontrolledOpen(open);
|
|
67
|
+
}
|
|
68
|
+
controlledOnOpenChange?.(open);
|
|
69
|
+
},
|
|
70
|
+
[lockScroll, isControlled, controlledOnOpenChange],
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Collapsible
|
|
75
|
+
ref={collapsibleRef}
|
|
76
|
+
data-slot="tool-fallback-root"
|
|
77
|
+
open={isOpen}
|
|
78
|
+
onOpenChange={handleOpenChange}
|
|
79
|
+
className={cn(
|
|
80
|
+
"aui-tool-fallback-root group/tool-fallback-root w-full",
|
|
81
|
+
className,
|
|
82
|
+
)}
|
|
83
|
+
style={
|
|
84
|
+
{
|
|
85
|
+
"--animation-duration": `${ANIMATION_DURATION}ms`,
|
|
86
|
+
} as React.CSSProperties
|
|
87
|
+
}
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
{children}
|
|
91
|
+
</Collapsible>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type ToolStatus = ToolCallMessagePartStatus["type"];
|
|
96
|
+
|
|
97
|
+
const statusIconMap: Record<ToolStatus, React.ElementType> = {
|
|
98
|
+
running: LoaderIcon,
|
|
99
|
+
complete: CheckIcon,
|
|
100
|
+
incomplete: XCircleIcon,
|
|
101
|
+
"requires-action": AlertCircleIcon,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const formatToolDuration = (ms: number) => {
|
|
105
|
+
if (ms < 1000) return "<1s";
|
|
106
|
+
const seconds = ms / 1000;
|
|
107
|
+
if (seconds < 10) return `${(Math.floor(seconds * 10) / 10).toFixed(1)}s`;
|
|
108
|
+
if (seconds < 60) return `${Math.floor(seconds)}s`;
|
|
109
|
+
return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
function ToolFallbackDuration({
|
|
113
|
+
className,
|
|
114
|
+
...props
|
|
115
|
+
}: React.ComponentProps<"span">) {
|
|
116
|
+
const elapsedMs = useToolCallElapsed();
|
|
117
|
+
if (elapsedMs === undefined) return null;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<span
|
|
121
|
+
data-slot="tool-fallback-duration"
|
|
122
|
+
className={cn(
|
|
123
|
+
"aui-tool-fallback-duration text-muted-foreground text-xs tabular-nums",
|
|
124
|
+
className,
|
|
125
|
+
)}
|
|
126
|
+
{...props}
|
|
127
|
+
>
|
|
128
|
+
{formatToolDuration(elapsedMs)}
|
|
129
|
+
</span>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ToolFallbackTrigger({
|
|
134
|
+
toolName,
|
|
135
|
+
status,
|
|
136
|
+
className,
|
|
137
|
+
...props
|
|
138
|
+
}: React.ComponentProps<typeof CollapsibleTrigger> & {
|
|
139
|
+
toolName: string;
|
|
140
|
+
status?: ToolCallMessagePartStatus;
|
|
141
|
+
}) {
|
|
142
|
+
const statusType = status?.type ?? "complete";
|
|
143
|
+
const isRunning = statusType === "running";
|
|
144
|
+
const isCancelled =
|
|
145
|
+
status?.type === "incomplete" && status.reason === "cancelled";
|
|
146
|
+
|
|
147
|
+
const Icon = statusIconMap[statusType];
|
|
148
|
+
const label =
|
|
149
|
+
statusType === "running"
|
|
150
|
+
? "Running tool"
|
|
151
|
+
: statusType === "requires-action"
|
|
152
|
+
? "Waiting on tool"
|
|
153
|
+
: statusType === "incomplete"
|
|
154
|
+
? `${isCancelled ? "Cancelled" : "Failed"} tool`
|
|
155
|
+
: "Used tool";
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<CollapsibleTrigger
|
|
159
|
+
data-slot="tool-fallback-trigger"
|
|
160
|
+
className={cn(
|
|
161
|
+
"aui-tool-fallback-trigger group/trigger text-muted-foreground hover:text-foreground flex w-fit origin-left items-center gap-2 py-1.5 text-sm transition-[color,scale] active:scale-[0.98]",
|
|
162
|
+
className,
|
|
163
|
+
)}
|
|
164
|
+
{...props}
|
|
165
|
+
>
|
|
166
|
+
<Icon
|
|
167
|
+
data-slot="tool-fallback-trigger-icon"
|
|
168
|
+
className={cn(
|
|
169
|
+
"aui-tool-fallback-trigger-icon size-4 shrink-0",
|
|
170
|
+
isCancelled && "text-muted-foreground",
|
|
171
|
+
isRunning && "animate-spin [animation-duration:0.6s]",
|
|
172
|
+
)}
|
|
173
|
+
/>
|
|
174
|
+
<span
|
|
175
|
+
data-slot="tool-fallback-trigger-label"
|
|
176
|
+
className={cn(
|
|
177
|
+
"aui-tool-fallback-trigger-label-wrapper inline-block text-start leading-none",
|
|
178
|
+
isCancelled && "text-muted-foreground line-through",
|
|
179
|
+
isRunning && "shimmer motion-reduce:animate-none",
|
|
180
|
+
)}
|
|
181
|
+
>
|
|
182
|
+
{label}: <b>{toolName}</b>
|
|
183
|
+
</span>
|
|
184
|
+
<ToolFallbackDuration />
|
|
185
|
+
<ChevronDownIcon
|
|
186
|
+
data-slot="tool-fallback-trigger-chevron"
|
|
187
|
+
className={cn(
|
|
188
|
+
"aui-tool-fallback-trigger-chevron size-4 shrink-0",
|
|
189
|
+
"transition-transform duration-(--animation-duration) ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:transition-none",
|
|
190
|
+
"-rotate-90",
|
|
191
|
+
"group-data-open/trigger:rotate-0",
|
|
192
|
+
"group-data-panel-open/trigger:rotate-0",
|
|
193
|
+
)}
|
|
194
|
+
/>
|
|
195
|
+
</CollapsibleTrigger>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function ToolFallbackContent({
|
|
200
|
+
className,
|
|
201
|
+
children,
|
|
202
|
+
...props
|
|
203
|
+
}: React.ComponentProps<typeof CollapsibleContent>) {
|
|
204
|
+
return (
|
|
205
|
+
<CollapsibleContent
|
|
206
|
+
data-slot="tool-fallback-content"
|
|
207
|
+
className={cn(
|
|
208
|
+
"aui-tool-fallback-content relative overflow-hidden text-sm outline-none",
|
|
209
|
+
"group/collapsible-content ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:animate-none",
|
|
210
|
+
"data-closed:animate-collapsible-up",
|
|
211
|
+
"data-open:animate-collapsible-down",
|
|
212
|
+
"data-closed:fill-mode-forwards",
|
|
213
|
+
"data-closed:pointer-events-none",
|
|
214
|
+
"[--tw-duration:var(--animation-duration)]",
|
|
215
|
+
className,
|
|
216
|
+
)}
|
|
217
|
+
{...props}
|
|
218
|
+
>
|
|
219
|
+
<div
|
|
220
|
+
className={cn(
|
|
221
|
+
"flex flex-col gap-2 ps-6 pt-1 pb-2 ease-[cubic-bezier(0.32,0.72,0,1)] motion-reduce:animate-none",
|
|
222
|
+
"group-data-open/collapsible-content:animate-in group-data-open/collapsible-content:fade-in-0 group-data-open/collapsible-content:blur-in-[2px] group-data-open/collapsible-content:slide-in-from-top-1",
|
|
223
|
+
"group-data-closed/collapsible-content:animate-out group-data-closed/collapsible-content:fade-out-0 group-data-closed/collapsible-content:blur-out-[2px] group-data-closed/collapsible-content:slide-out-to-top-1",
|
|
224
|
+
"group-data-closed/collapsible-content:animation-duration-(--animation-duration) group-data-open/collapsible-content:animation-duration-(--animation-duration)",
|
|
225
|
+
)}
|
|
226
|
+
>
|
|
227
|
+
{children}
|
|
228
|
+
</div>
|
|
229
|
+
</CollapsibleContent>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function ToolFallbackArgs({
|
|
234
|
+
argsText,
|
|
235
|
+
className,
|
|
236
|
+
...props
|
|
237
|
+
}: React.ComponentProps<"div"> & {
|
|
238
|
+
argsText?: string;
|
|
239
|
+
}) {
|
|
240
|
+
if (!argsText) return null;
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<div
|
|
244
|
+
data-slot="tool-fallback-args"
|
|
245
|
+
className={cn("aui-tool-fallback-args", className)}
|
|
246
|
+
{...props}
|
|
247
|
+
>
|
|
248
|
+
<pre className="aui-tool-fallback-args-value bg-muted/50 text-foreground/90 rounded-md p-2.5 text-xs whitespace-pre-wrap">
|
|
249
|
+
{argsText}
|
|
250
|
+
</pre>
|
|
251
|
+
</div>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const formatUnknownValue = (value: unknown, space?: number): string => {
|
|
256
|
+
if (typeof value === "string") return value;
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
if (value instanceof Error) return String(value);
|
|
260
|
+
|
|
261
|
+
const json = JSON.stringify(value, null, space);
|
|
262
|
+
if (json !== undefined) return json;
|
|
263
|
+
} catch {}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
return String(value);
|
|
267
|
+
} catch {
|
|
268
|
+
return "[Unserializable value]";
|
|
269
|
+
}
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
function ToolFallbackResult({
|
|
273
|
+
result,
|
|
274
|
+
className,
|
|
275
|
+
...props
|
|
276
|
+
}: React.ComponentProps<"div"> & {
|
|
277
|
+
result?: unknown;
|
|
278
|
+
}) {
|
|
279
|
+
if (result === undefined) return null;
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div
|
|
283
|
+
data-slot="tool-fallback-result"
|
|
284
|
+
className={cn("aui-tool-fallback-result", className)}
|
|
285
|
+
{...props}
|
|
286
|
+
>
|
|
287
|
+
<p className="aui-tool-fallback-result-header text-muted-foreground text-xs font-medium">
|
|
288
|
+
Result:
|
|
289
|
+
</p>
|
|
290
|
+
<pre className="aui-tool-fallback-result-content bg-muted/50 text-foreground/90 mt-1 rounded-md p-2.5 text-xs whitespace-pre-wrap">
|
|
291
|
+
{formatUnknownValue(result, 2)}
|
|
292
|
+
</pre>
|
|
293
|
+
</div>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function ToolFallbackError({
|
|
298
|
+
status,
|
|
299
|
+
className,
|
|
300
|
+
...props
|
|
301
|
+
}: React.ComponentProps<"div"> & {
|
|
302
|
+
status?: ToolCallMessagePartStatus;
|
|
303
|
+
}) {
|
|
304
|
+
if (status?.type !== "incomplete") return null;
|
|
305
|
+
|
|
306
|
+
const error = status.error;
|
|
307
|
+
const errorText =
|
|
308
|
+
error === undefined || error === null ? null : formatUnknownValue(error);
|
|
309
|
+
|
|
310
|
+
if (!errorText) return null;
|
|
311
|
+
|
|
312
|
+
const isCancelled = status.reason === "cancelled";
|
|
313
|
+
const headerText = isCancelled ? "Cancelled reason:" : "Error:";
|
|
314
|
+
|
|
315
|
+
return (
|
|
316
|
+
<div
|
|
317
|
+
data-slot="tool-fallback-error"
|
|
318
|
+
className={cn("aui-tool-fallback-error", className)}
|
|
319
|
+
{...props}
|
|
320
|
+
>
|
|
321
|
+
<p className="aui-tool-fallback-error-header text-muted-foreground font-semibold">
|
|
322
|
+
{headerText}
|
|
323
|
+
</p>
|
|
324
|
+
<p className="aui-tool-fallback-error-reason text-muted-foreground whitespace-pre-line">
|
|
325
|
+
{errorText}
|
|
326
|
+
</p>
|
|
327
|
+
</div>
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const APPROVED_RESULT = "Approved by user";
|
|
332
|
+
const DENIED_RESULT = "User denied tool execution";
|
|
333
|
+
|
|
334
|
+
const APPROVAL_OPTION_DEFAULT_LABELS: Record<string, string> = {
|
|
335
|
+
"allow-once": "Allow",
|
|
336
|
+
"allow-always": "Always allow",
|
|
337
|
+
"reject-once": "Deny",
|
|
338
|
+
"reject-always": "Always deny",
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const isKnownKind = (kind: string) =>
|
|
342
|
+
Object.hasOwn(APPROVAL_OPTION_DEFAULT_LABELS, kind);
|
|
343
|
+
|
|
344
|
+
const isAllowKind = (kind: string) =>
|
|
345
|
+
kind === "allow-once" || kind === "allow-always";
|
|
346
|
+
|
|
347
|
+
const approvalOptionLabel = (option: ToolApprovalOption) =>
|
|
348
|
+
option.label ??
|
|
349
|
+
(isKnownKind(option.kind)
|
|
350
|
+
? APPROVAL_OPTION_DEFAULT_LABELS[option.kind]
|
|
351
|
+
: undefined) ??
|
|
352
|
+
option.id;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* A request that declares how it wants to be presented is asking a question,
|
|
356
|
+
* not gating an action, so a refusal is not one of the answers it accepts
|
|
357
|
+
* unless the request declares itself dismissible.
|
|
358
|
+
*/
|
|
359
|
+
const isQuestion = (approval: ToolCallMessagePart["approval"]) =>
|
|
360
|
+
approval?.display === "select" ||
|
|
361
|
+
approval?.display === "text" ||
|
|
362
|
+
approval?.display === "questions";
|
|
363
|
+
|
|
364
|
+
const questionAcceptsText = (question: ToolApprovalQuestion) =>
|
|
365
|
+
!question.options?.length || question.allowFreeform === true;
|
|
366
|
+
|
|
367
|
+
const answerLabels = (
|
|
368
|
+
question: ToolApprovalQuestion,
|
|
369
|
+
answer: ToolApprovalAnswer | undefined,
|
|
370
|
+
) => [
|
|
371
|
+
...(answer?.optionIds ?? []).map(
|
|
372
|
+
(id) => question.options?.find((option) => option.id === id)?.label ?? id,
|
|
373
|
+
),
|
|
374
|
+
...(answer?.text?.trim() ? [answer.text] : []),
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
const isSettled = (approval: ToolCallMessagePart["approval"]) =>
|
|
378
|
+
approval != null &&
|
|
379
|
+
(approval.approved !== undefined || approval.resolution !== undefined);
|
|
380
|
+
|
|
381
|
+
type ApprovalReceipt = {
|
|
382
|
+
outcome: "allowed" | "refused" | "closed";
|
|
383
|
+
label: string;
|
|
384
|
+
option?: string | undefined;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* A settled request reads as a past-tense record of what happened to it, so
|
|
389
|
+
* scrolling back never shows a live control for a decision already made.
|
|
390
|
+
*/
|
|
391
|
+
const approvalReceipt = (
|
|
392
|
+
approval: NonNullable<ToolCallMessagePart["approval"]>,
|
|
393
|
+
): ApprovalReceipt => {
|
|
394
|
+
if (approval.resolution !== undefined)
|
|
395
|
+
return {
|
|
396
|
+
outcome: "closed",
|
|
397
|
+
label:
|
|
398
|
+
approval.resolution === "cancelled"
|
|
399
|
+
? "Cancelled before a decision"
|
|
400
|
+
: "Expired before a decision",
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const chosen =
|
|
404
|
+
approval.optionId === undefined
|
|
405
|
+
? undefined
|
|
406
|
+
: approval.options?.find((option) => option.id === approval.optionId);
|
|
407
|
+
const option =
|
|
408
|
+
chosen !== undefined
|
|
409
|
+
? approvalOptionLabel(chosen)
|
|
410
|
+
: approval.optionId === undefined
|
|
411
|
+
? undefined
|
|
412
|
+
: approval.optionId;
|
|
413
|
+
const answered =
|
|
414
|
+
isQuestion(approval) || (chosen !== undefined && !isKnownKind(chosen.kind));
|
|
415
|
+
const automatic = approval.isAutomatic ? " automatically" : "";
|
|
416
|
+
|
|
417
|
+
if (approval.approved)
|
|
418
|
+
return {
|
|
419
|
+
outcome: "allowed",
|
|
420
|
+
label: `${answered ? "Answered" : "Allowed"}${automatic}`,
|
|
421
|
+
option,
|
|
422
|
+
};
|
|
423
|
+
return {
|
|
424
|
+
outcome: "refused",
|
|
425
|
+
label: `${answered ? "Dismissed" : "Denied"}${automatic}`,
|
|
426
|
+
option,
|
|
427
|
+
};
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const receiptIcons = {
|
|
431
|
+
allowed: CheckIcon,
|
|
432
|
+
refused: XCircleIcon,
|
|
433
|
+
closed: CircleMinusIcon,
|
|
434
|
+
} satisfies Record<ApprovalReceipt["outcome"], React.ElementType>;
|
|
435
|
+
|
|
436
|
+
function ToolFallbackApprovalReceipt({
|
|
437
|
+
approval,
|
|
438
|
+
className,
|
|
439
|
+
...props
|
|
440
|
+
}: React.ComponentProps<"div"> & {
|
|
441
|
+
approval: NonNullable<ToolCallMessagePart["approval"]>;
|
|
442
|
+
}) {
|
|
443
|
+
const receipt = approvalReceipt(approval);
|
|
444
|
+
const Icon = receiptIcons[receipt.outcome];
|
|
445
|
+
const notes = [
|
|
446
|
+
...new Set(
|
|
447
|
+
[approval.text, approval.reason].filter(
|
|
448
|
+
(value): value is string => typeof value === "string" && value !== "",
|
|
449
|
+
),
|
|
450
|
+
),
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<div
|
|
455
|
+
data-slot="tool-fallback-approval-receipt"
|
|
456
|
+
data-outcome={receipt.outcome}
|
|
457
|
+
className={cn(
|
|
458
|
+
"aui-tool-fallback-approval-receipt flex flex-col gap-1.5 pt-1",
|
|
459
|
+
className,
|
|
460
|
+
)}
|
|
461
|
+
{...props}
|
|
462
|
+
>
|
|
463
|
+
{approval.prompt ? (
|
|
464
|
+
<p className="aui-tool-fallback-approval-prompt text-muted-foreground whitespace-pre-line">
|
|
465
|
+
{approval.prompt}
|
|
466
|
+
</p>
|
|
467
|
+
) : null}
|
|
468
|
+
<p className="aui-tool-fallback-approval-receipt-label flex items-center gap-1.5">
|
|
469
|
+
<Icon aria-hidden className="text-muted-foreground size-3.5 shrink-0" />
|
|
470
|
+
<span className="font-medium">{receipt.label}</span>
|
|
471
|
+
{receipt.option !== undefined ? (
|
|
472
|
+
<span className="text-muted-foreground">· {receipt.option}</span>
|
|
473
|
+
) : null}
|
|
474
|
+
</p>
|
|
475
|
+
{approval.answers &&
|
|
476
|
+
approval.questions?.map((question) => {
|
|
477
|
+
const labels = answerLabels(
|
|
478
|
+
question,
|
|
479
|
+
approval.answers && Object.hasOwn(approval.answers, question.id)
|
|
480
|
+
? approval.answers[question.id]
|
|
481
|
+
: undefined,
|
|
482
|
+
);
|
|
483
|
+
if (labels.length === 0) return null;
|
|
484
|
+
return (
|
|
485
|
+
<p
|
|
486
|
+
key={question.id}
|
|
487
|
+
className="aui-tool-fallback-approval-receipt-answer whitespace-pre-line"
|
|
488
|
+
>
|
|
489
|
+
<span className="text-muted-foreground">
|
|
490
|
+
{question.header ?? question.prompt}
|
|
491
|
+
</span>{" "}
|
|
492
|
+
· {labels.join(", ")}
|
|
493
|
+
</p>
|
|
494
|
+
);
|
|
495
|
+
})}
|
|
496
|
+
{notes.map((text) => (
|
|
497
|
+
<p
|
|
498
|
+
key={text}
|
|
499
|
+
className="aui-tool-fallback-approval-receipt-note text-muted-foreground whitespace-pre-line"
|
|
500
|
+
>
|
|
501
|
+
{text}
|
|
502
|
+
</p>
|
|
503
|
+
))}
|
|
504
|
+
</div>
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function ToolFallbackApprovalQuestions({
|
|
509
|
+
questions,
|
|
510
|
+
dismissible,
|
|
511
|
+
locked,
|
|
512
|
+
onSend,
|
|
513
|
+
onDismiss,
|
|
514
|
+
}: {
|
|
515
|
+
questions: readonly ToolApprovalQuestion[];
|
|
516
|
+
dismissible: boolean;
|
|
517
|
+
locked: boolean;
|
|
518
|
+
onSend: (answers: Record<string, ToolApprovalAnswer>) => void;
|
|
519
|
+
onDismiss: () => void;
|
|
520
|
+
}) {
|
|
521
|
+
const [selected, setSelected] = useState<
|
|
522
|
+
ReadonlyMap<string, readonly string[]>
|
|
523
|
+
>(() => new Map());
|
|
524
|
+
const [typed, setTyped] = useState<ReadonlyMap<string, string>>(
|
|
525
|
+
() => new Map(),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
const toggle = (question: ToolApprovalQuestion, optionId: string) =>
|
|
529
|
+
setSelected((current) => {
|
|
530
|
+
const chosen = current.get(question.id) ?? [];
|
|
531
|
+
const next = chosen.includes(optionId)
|
|
532
|
+
? chosen.filter((id) => id !== optionId)
|
|
533
|
+
: question.multiple
|
|
534
|
+
? [...chosen, optionId]
|
|
535
|
+
: [optionId];
|
|
536
|
+
return new Map(current).set(question.id, next);
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const answerOf = (question: ToolApprovalQuestion): ToolApprovalAnswer => {
|
|
540
|
+
const optionIds = selected.get(question.id) ?? [];
|
|
541
|
+
const draft = typed.get(question.id);
|
|
542
|
+
const text = draft?.trim() ? draft : undefined;
|
|
543
|
+
return {
|
|
544
|
+
...(optionIds.length > 0 && { optionIds }),
|
|
545
|
+
...(text !== undefined && { text }),
|
|
546
|
+
};
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const complete =
|
|
550
|
+
questions.length > 0 &&
|
|
551
|
+
questions.every((question) => {
|
|
552
|
+
const answer = answerOf(question);
|
|
553
|
+
return answer.optionIds !== undefined || answer.text !== undefined;
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
const send = () => {
|
|
557
|
+
if (locked || !complete) return;
|
|
558
|
+
onSend(
|
|
559
|
+
Object.fromEntries(
|
|
560
|
+
questions.map((question) => [question.id, answerOf(question)]),
|
|
561
|
+
),
|
|
562
|
+
);
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
return (
|
|
566
|
+
<>
|
|
567
|
+
{questions.map((question) => {
|
|
568
|
+
const chosen = selected.get(question.id) ?? [];
|
|
569
|
+
const described =
|
|
570
|
+
question.options?.some((option) => option.description) ?? false;
|
|
571
|
+
return (
|
|
572
|
+
<div
|
|
573
|
+
key={question.id}
|
|
574
|
+
role="group"
|
|
575
|
+
aria-label={question.prompt}
|
|
576
|
+
data-slot="tool-fallback-approval-question"
|
|
577
|
+
className="aui-tool-fallback-approval-question flex flex-col gap-2"
|
|
578
|
+
>
|
|
579
|
+
<p className="aui-tool-fallback-approval-question-prompt text-foreground whitespace-pre-line">
|
|
580
|
+
{question.header ? (
|
|
581
|
+
<span className="text-muted-foreground me-1.5 text-xs font-medium uppercase">
|
|
582
|
+
{question.header}
|
|
583
|
+
</span>
|
|
584
|
+
) : null}
|
|
585
|
+
{question.prompt}
|
|
586
|
+
</p>
|
|
587
|
+
{question.options && question.options.length > 0 ? (
|
|
588
|
+
<div
|
|
589
|
+
className={cn(
|
|
590
|
+
"flex gap-2",
|
|
591
|
+
described
|
|
592
|
+
? "flex-col items-stretch"
|
|
593
|
+
: "flex-wrap items-center",
|
|
594
|
+
)}
|
|
595
|
+
>
|
|
596
|
+
{question.options.map((option) => {
|
|
597
|
+
const pressed = chosen.includes(option.id);
|
|
598
|
+
return (
|
|
599
|
+
<Button
|
|
600
|
+
key={option.id}
|
|
601
|
+
size="sm"
|
|
602
|
+
variant={pressed ? "default" : "outline"}
|
|
603
|
+
className={cn(
|
|
604
|
+
pressable,
|
|
605
|
+
described &&
|
|
606
|
+
"h-auto flex-col items-start gap-0.5 py-2 text-start whitespace-normal",
|
|
607
|
+
)}
|
|
608
|
+
aria-pressed={pressed}
|
|
609
|
+
onClick={() => toggle(question, option.id)}
|
|
610
|
+
disabled={locked}
|
|
611
|
+
>
|
|
612
|
+
<span>{option.label}</span>
|
|
613
|
+
{option.description ? (
|
|
614
|
+
<span className="text-xs font-normal opacity-80">
|
|
615
|
+
{option.description}
|
|
616
|
+
</span>
|
|
617
|
+
) : null}
|
|
618
|
+
</Button>
|
|
619
|
+
);
|
|
620
|
+
})}
|
|
621
|
+
</div>
|
|
622
|
+
) : null}
|
|
623
|
+
{questionAcceptsText(question) ? (
|
|
624
|
+
<Textarea
|
|
625
|
+
value={typed.get(question.id) ?? ""}
|
|
626
|
+
onChange={(event) =>
|
|
627
|
+
setTyped((current) =>
|
|
628
|
+
new Map(current).set(question.id, event.target.value),
|
|
629
|
+
)
|
|
630
|
+
}
|
|
631
|
+
disabled={locked}
|
|
632
|
+
aria-label={question.prompt}
|
|
633
|
+
placeholder={
|
|
634
|
+
question.options?.length
|
|
635
|
+
? "Or type an answer"
|
|
636
|
+
: "Type your answer"
|
|
637
|
+
}
|
|
638
|
+
/>
|
|
639
|
+
) : null}
|
|
640
|
+
</div>
|
|
641
|
+
);
|
|
642
|
+
})}
|
|
643
|
+
<div className="flex items-center gap-2">
|
|
644
|
+
<Button
|
|
645
|
+
size="sm"
|
|
646
|
+
className={pressable}
|
|
647
|
+
onClick={send}
|
|
648
|
+
disabled={locked || !complete}
|
|
649
|
+
>
|
|
650
|
+
Send
|
|
651
|
+
</Button>
|
|
652
|
+
{dismissible ? (
|
|
653
|
+
<Button
|
|
654
|
+
size="sm"
|
|
655
|
+
variant="outline"
|
|
656
|
+
className={pressable}
|
|
657
|
+
onClick={onDismiss}
|
|
658
|
+
disabled={locked}
|
|
659
|
+
>
|
|
660
|
+
Dismiss
|
|
661
|
+
</Button>
|
|
662
|
+
) : null}
|
|
663
|
+
</div>
|
|
664
|
+
</>
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const offersInterruptAction = (
|
|
669
|
+
status: ToolCallMessagePartStatus | undefined,
|
|
670
|
+
approval: ToolCallMessagePart["approval"],
|
|
671
|
+
interrupt: ToolCallMessagePart["interrupt"],
|
|
672
|
+
) =>
|
|
673
|
+
status?.type !== "requires-action" ||
|
|
674
|
+
status.reason !== "interrupt" ||
|
|
675
|
+
approval != null ||
|
|
676
|
+
interrupt != null;
|
|
677
|
+
|
|
678
|
+
function ToolFallbackApproval(
|
|
679
|
+
props: React.ComponentProps<typeof ToolFallbackApprovalImpl>,
|
|
680
|
+
) {
|
|
681
|
+
return <ToolFallbackApprovalImpl key={props.approval?.id} {...props} />;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function ToolFallbackApprovalImpl({
|
|
685
|
+
className,
|
|
686
|
+
addResult,
|
|
687
|
+
resume,
|
|
688
|
+
interrupt,
|
|
689
|
+
approval,
|
|
690
|
+
respondToApproval,
|
|
691
|
+
status,
|
|
692
|
+
...props
|
|
693
|
+
}: React.ComponentProps<"div"> &
|
|
694
|
+
Partial<
|
|
695
|
+
Pick<
|
|
696
|
+
ToolCallMessagePartProps,
|
|
697
|
+
"addResult" | "resume" | "respondToApproval" | "status"
|
|
698
|
+
>
|
|
699
|
+
> & {
|
|
700
|
+
interrupt?: ToolCallMessagePart["interrupt"];
|
|
701
|
+
approval?: ToolCallMessagePart["approval"];
|
|
702
|
+
}) {
|
|
703
|
+
const [submitted, setSubmitted] = useState(false);
|
|
704
|
+
const voiceActive = useAuiState((s) => s.thread.voice !== undefined);
|
|
705
|
+
const canAnswer = useAuiState((s) => s.thread.capabilities.answerToolCall);
|
|
706
|
+
const locked = submitted || voiceActive;
|
|
707
|
+
const [confirmingId, setConfirmingId] = useState<string | null>(null);
|
|
708
|
+
const [answer, setAnswer] = useState("");
|
|
709
|
+
const [error, setError] = useState<string | null>(null);
|
|
710
|
+
|
|
711
|
+
if (approval != null && isSettled(approval))
|
|
712
|
+
return (
|
|
713
|
+
<ToolFallbackApprovalReceipt
|
|
714
|
+
approval={approval}
|
|
715
|
+
className={className}
|
|
716
|
+
{...props}
|
|
717
|
+
/>
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
if (!offersInterruptAction(status, approval, interrupt)) return null;
|
|
721
|
+
|
|
722
|
+
const promptText = approval?.prompt ? (
|
|
723
|
+
<p className="aui-tool-fallback-approval-prompt text-foreground whitespace-pre-line">
|
|
724
|
+
{approval.prompt}
|
|
725
|
+
</p>
|
|
726
|
+
) : null;
|
|
727
|
+
|
|
728
|
+
if (!canAnswer)
|
|
729
|
+
return (
|
|
730
|
+
promptText && (
|
|
731
|
+
<div
|
|
732
|
+
data-slot="tool-fallback-approval"
|
|
733
|
+
className={cn(
|
|
734
|
+
"aui-tool-fallback-approval flex flex-col gap-2 pt-1",
|
|
735
|
+
className,
|
|
736
|
+
)}
|
|
737
|
+
{...props}
|
|
738
|
+
>
|
|
739
|
+
{promptText}
|
|
740
|
+
</div>
|
|
741
|
+
)
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
// A declared option list is a host constraint: the kit never adds an
|
|
745
|
+
// approval path beyond it, and preserves a refusal path only where the
|
|
746
|
+
// request is an action the user may refuse.
|
|
747
|
+
const declaredOptions = respondToApproval ? approval?.options : undefined;
|
|
748
|
+
const acceptsText =
|
|
749
|
+
approval != null &&
|
|
750
|
+
respondToApproval != null &&
|
|
751
|
+
toolApprovalAcceptsText(approval);
|
|
752
|
+
|
|
753
|
+
// A refused response leaves the request open, so the controls come back
|
|
754
|
+
// rather than staying spent on a decision the runtime never recorded.
|
|
755
|
+
const submit = (send: () => Promise<void> | void) => {
|
|
756
|
+
setSubmitted(true);
|
|
757
|
+
setError(null);
|
|
758
|
+
void (async () => {
|
|
759
|
+
try {
|
|
760
|
+
await send();
|
|
761
|
+
} catch (sendError) {
|
|
762
|
+
setSubmitted(false);
|
|
763
|
+
setError(
|
|
764
|
+
sendError instanceof Error ? sendError.message : String(sendError),
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
})();
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
const respond = (approved: boolean) => {
|
|
771
|
+
if (locked) return;
|
|
772
|
+
if (
|
|
773
|
+
approval != null &&
|
|
774
|
+
approval.approved === undefined &&
|
|
775
|
+
respondToApproval
|
|
776
|
+
) {
|
|
777
|
+
submit(() => respondToApproval({ approved, ...typedNote() }));
|
|
778
|
+
} else if (interrupt) {
|
|
779
|
+
submit(() => resume?.({ approved }));
|
|
780
|
+
} else if (
|
|
781
|
+
status?.type === "requires-action" &&
|
|
782
|
+
status.reason === "interrupt"
|
|
783
|
+
) {
|
|
784
|
+
return;
|
|
785
|
+
} else {
|
|
786
|
+
submit(() => addResult?.(approved ? APPROVED_RESULT : DENIED_RESULT));
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
const respondWithOption = (option: ToolApprovalOption) => {
|
|
791
|
+
if (locked) return;
|
|
792
|
+
setConfirmingId(null);
|
|
793
|
+
// A custom kind has no decision class for the runtime to derive, and
|
|
794
|
+
// responding without one throws; picking a declared option is an answer,
|
|
795
|
+
// so it resolves as approved.
|
|
796
|
+
submit(() =>
|
|
797
|
+
respondToApproval?.(
|
|
798
|
+
isKnownKind(option.kind)
|
|
799
|
+
? { optionId: option.id, ...typedNote() }
|
|
800
|
+
: { optionId: option.id, approved: true, ...typedNote() },
|
|
801
|
+
),
|
|
802
|
+
);
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
const typedNote = () => (answer.trim() ? { text: answer } : {});
|
|
806
|
+
|
|
807
|
+
// The kit does not validate an answer the request never constrained: a host
|
|
808
|
+
// that cannot record an empty one rejects it, which reopens the controls.
|
|
809
|
+
const submitAnswer = () => {
|
|
810
|
+
if (locked) return;
|
|
811
|
+
submit(() => respondToApproval?.({ text: answer }));
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
// A dismissal is no answer at all, so a typed draft does not travel with it.
|
|
815
|
+
const dismiss = () => {
|
|
816
|
+
if (locked) return;
|
|
817
|
+
submit(() => respondToApproval?.({ approved: false }));
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
const handleOption = (option: ToolApprovalOption) => {
|
|
821
|
+
if (option.confirm) {
|
|
822
|
+
setConfirmingId(option.id);
|
|
823
|
+
} else {
|
|
824
|
+
respondWithOption(option);
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
const confirming =
|
|
829
|
+
confirmingId != null
|
|
830
|
+
? declaredOptions?.find((o) => o.id === confirmingId)
|
|
831
|
+
: undefined;
|
|
832
|
+
|
|
833
|
+
const question = isQuestion(approval);
|
|
834
|
+
const dismissible =
|
|
835
|
+
question && respondToApproval != null && approval?.dismissible === true;
|
|
836
|
+
|
|
837
|
+
const dismissButton = dismissible ? (
|
|
838
|
+
<Button
|
|
839
|
+
size="sm"
|
|
840
|
+
variant="outline"
|
|
841
|
+
className={pressable}
|
|
842
|
+
onClick={dismiss}
|
|
843
|
+
disabled={locked}
|
|
844
|
+
>
|
|
845
|
+
Dismiss
|
|
846
|
+
</Button>
|
|
847
|
+
) : null;
|
|
848
|
+
|
|
849
|
+
const errorText = error ? (
|
|
850
|
+
<p
|
|
851
|
+
role="alert"
|
|
852
|
+
className="aui-tool-fallback-approval-error text-destructive text-xs whitespace-pre-line"
|
|
853
|
+
>
|
|
854
|
+
{error}
|
|
855
|
+
</p>
|
|
856
|
+
) : null;
|
|
857
|
+
|
|
858
|
+
const answerField = acceptsText ? (
|
|
859
|
+
<div className="aui-tool-fallback-approval-answer flex flex-col items-start gap-2">
|
|
860
|
+
<Textarea
|
|
861
|
+
value={answer}
|
|
862
|
+
onChange={(event) => setAnswer(event.target.value)}
|
|
863
|
+
disabled={locked}
|
|
864
|
+
aria-label={question ? (approval?.prompt ?? "Answer") : "Note"}
|
|
865
|
+
placeholder={
|
|
866
|
+
question ? "Type your answer" : "Add a note to your decision"
|
|
867
|
+
}
|
|
868
|
+
/>
|
|
869
|
+
{question && (
|
|
870
|
+
<div className="flex items-center gap-2">
|
|
871
|
+
<Button
|
|
872
|
+
size="sm"
|
|
873
|
+
className={pressable}
|
|
874
|
+
onClick={submitAnswer}
|
|
875
|
+
disabled={locked}
|
|
876
|
+
>
|
|
877
|
+
Send
|
|
878
|
+
</Button>
|
|
879
|
+
{dismissButton}
|
|
880
|
+
</div>
|
|
881
|
+
)}
|
|
882
|
+
</div>
|
|
883
|
+
) : null;
|
|
884
|
+
|
|
885
|
+
if (approval?.display === "questions" && respondToApproval) {
|
|
886
|
+
return (
|
|
887
|
+
<div
|
|
888
|
+
data-slot="tool-fallback-approval"
|
|
889
|
+
className={cn(
|
|
890
|
+
"aui-tool-fallback-approval flex flex-col gap-3 pt-1",
|
|
891
|
+
className,
|
|
892
|
+
)}
|
|
893
|
+
{...props}
|
|
894
|
+
>
|
|
895
|
+
{promptText}
|
|
896
|
+
<ToolFallbackApprovalQuestions
|
|
897
|
+
questions={approval.questions ?? []}
|
|
898
|
+
dismissible={approval.dismissible === true}
|
|
899
|
+
locked={locked}
|
|
900
|
+
onSend={(answers) => submit(() => respondToApproval({ answers }))}
|
|
901
|
+
onDismiss={dismiss}
|
|
902
|
+
/>
|
|
903
|
+
{errorText}
|
|
904
|
+
</div>
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
if (confirming) {
|
|
909
|
+
const confirmMeta =
|
|
910
|
+
typeof confirming.confirm === "object" ? confirming.confirm : undefined;
|
|
911
|
+
const confirmDescription =
|
|
912
|
+
confirmMeta?.description ?? confirming.description;
|
|
913
|
+
return (
|
|
914
|
+
<div
|
|
915
|
+
data-slot="tool-fallback-approval-confirm"
|
|
916
|
+
className={cn(
|
|
917
|
+
"aui-tool-fallback-approval-confirm flex flex-col gap-2 pt-1",
|
|
918
|
+
className,
|
|
919
|
+
)}
|
|
920
|
+
{...props}
|
|
921
|
+
>
|
|
922
|
+
<p className="aui-tool-fallback-approval-confirm-title font-semibold">
|
|
923
|
+
{confirmMeta?.title ?? `${approvalOptionLabel(confirming)}?`}
|
|
924
|
+
</p>
|
|
925
|
+
{confirmDescription && (
|
|
926
|
+
<p className="aui-tool-fallback-approval-confirm-description text-muted-foreground whitespace-pre-line">
|
|
927
|
+
{confirmDescription}
|
|
928
|
+
</p>
|
|
929
|
+
)}
|
|
930
|
+
{confirming.grants && confirming.grants.length > 0 && (
|
|
931
|
+
<ul className="aui-tool-fallback-approval-confirm-grants flex flex-col gap-1">
|
|
932
|
+
{confirming.grants.map((grant) => (
|
|
933
|
+
<li key={grant}>
|
|
934
|
+
<code className="aui-tool-fallback-approval-confirm-grant bg-muted rounded px-1.5 py-0.5 text-xs">
|
|
935
|
+
{grant}
|
|
936
|
+
</code>
|
|
937
|
+
</li>
|
|
938
|
+
))}
|
|
939
|
+
</ul>
|
|
940
|
+
)}
|
|
941
|
+
<div className="flex items-center gap-2">
|
|
942
|
+
<Button
|
|
943
|
+
size="sm"
|
|
944
|
+
className={pressable}
|
|
945
|
+
onClick={() => respondWithOption(confirming)}
|
|
946
|
+
disabled={locked}
|
|
947
|
+
>
|
|
948
|
+
Confirm
|
|
949
|
+
</Button>
|
|
950
|
+
<Button
|
|
951
|
+
size="sm"
|
|
952
|
+
variant="outline"
|
|
953
|
+
className={pressable}
|
|
954
|
+
onClick={() => setConfirmingId(null)}
|
|
955
|
+
disabled={locked}
|
|
956
|
+
>
|
|
957
|
+
Back
|
|
958
|
+
</Button>
|
|
959
|
+
</div>
|
|
960
|
+
</div>
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
if (declaredOptions && declaredOptions.length > 0) {
|
|
965
|
+
const allowOptions = declaredOptions.filter((o) => isAllowKind(o.kind));
|
|
966
|
+
const customOptions = declaredOptions.filter((o) => !isKnownKind(o.kind));
|
|
967
|
+
const rejectOptions = declaredOptions.filter(
|
|
968
|
+
(o) => isKnownKind(o.kind) && !isAllowKind(o.kind),
|
|
969
|
+
);
|
|
970
|
+
return (
|
|
971
|
+
<div
|
|
972
|
+
data-slot="tool-fallback-approval"
|
|
973
|
+
className={cn(
|
|
974
|
+
"aui-tool-fallback-approval flex flex-col gap-2 pt-1",
|
|
975
|
+
className,
|
|
976
|
+
)}
|
|
977
|
+
{...props}
|
|
978
|
+
>
|
|
979
|
+
{promptText}
|
|
980
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
981
|
+
{[...allowOptions, ...customOptions, ...rejectOptions].map(
|
|
982
|
+
(option) => (
|
|
983
|
+
<Button
|
|
984
|
+
key={option.id}
|
|
985
|
+
size="sm"
|
|
986
|
+
variant={option === allowOptions[0] ? "default" : "outline"}
|
|
987
|
+
className={pressable}
|
|
988
|
+
onClick={() => handleOption(option)}
|
|
989
|
+
disabled={locked}
|
|
990
|
+
>
|
|
991
|
+
{approvalOptionLabel(option)}
|
|
992
|
+
</Button>
|
|
993
|
+
),
|
|
994
|
+
)}
|
|
995
|
+
{rejectOptions.length === 0 && !question && (
|
|
996
|
+
<Button
|
|
997
|
+
size="sm"
|
|
998
|
+
variant="outline"
|
|
999
|
+
className={pressable}
|
|
1000
|
+
onClick={() => respond(false)}
|
|
1001
|
+
disabled={locked}
|
|
1002
|
+
>
|
|
1003
|
+
Deny
|
|
1004
|
+
</Button>
|
|
1005
|
+
)}
|
|
1006
|
+
{!acceptsText && dismissButton}
|
|
1007
|
+
</div>
|
|
1008
|
+
{answerField}
|
|
1009
|
+
{errorText}
|
|
1010
|
+
</div>
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// A question carries no decision to fabricate, so it renders only what the
|
|
1015
|
+
// request declared, even when that leaves nothing to act on here.
|
|
1016
|
+
if (question) {
|
|
1017
|
+
return (
|
|
1018
|
+
<div
|
|
1019
|
+
data-slot="tool-fallback-approval"
|
|
1020
|
+
className={cn(
|
|
1021
|
+
"aui-tool-fallback-approval flex flex-col gap-2 pt-1",
|
|
1022
|
+
className,
|
|
1023
|
+
)}
|
|
1024
|
+
{...props}
|
|
1025
|
+
>
|
|
1026
|
+
{promptText}
|
|
1027
|
+
{answerField}
|
|
1028
|
+
{!acceptsText && dismissButton && (
|
|
1029
|
+
<div className="flex items-center gap-2">{dismissButton}</div>
|
|
1030
|
+
)}
|
|
1031
|
+
{errorText}
|
|
1032
|
+
</div>
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return (
|
|
1037
|
+
<div
|
|
1038
|
+
data-slot="tool-fallback-approval"
|
|
1039
|
+
className={cn(
|
|
1040
|
+
"aui-tool-fallback-approval flex flex-col gap-2 pt-1",
|
|
1041
|
+
className,
|
|
1042
|
+
)}
|
|
1043
|
+
{...props}
|
|
1044
|
+
>
|
|
1045
|
+
{promptText}
|
|
1046
|
+
<div className="flex items-center gap-2">
|
|
1047
|
+
<Button
|
|
1048
|
+
size="sm"
|
|
1049
|
+
className={pressable}
|
|
1050
|
+
onClick={() => respond(true)}
|
|
1051
|
+
disabled={locked}
|
|
1052
|
+
>
|
|
1053
|
+
Allow
|
|
1054
|
+
</Button>
|
|
1055
|
+
<Button
|
|
1056
|
+
size="sm"
|
|
1057
|
+
variant="outline"
|
|
1058
|
+
className={pressable}
|
|
1059
|
+
onClick={() => respond(false)}
|
|
1060
|
+
disabled={locked}
|
|
1061
|
+
>
|
|
1062
|
+
Deny
|
|
1063
|
+
</Button>
|
|
1064
|
+
</div>
|
|
1065
|
+
{answerField}
|
|
1066
|
+
{errorText}
|
|
1067
|
+
</div>
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
const ToolFallbackImpl: ToolCallMessagePartComponent = ({
|
|
1072
|
+
toolName,
|
|
1073
|
+
argsText,
|
|
1074
|
+
result,
|
|
1075
|
+
status,
|
|
1076
|
+
addResult,
|
|
1077
|
+
resume,
|
|
1078
|
+
interrupt,
|
|
1079
|
+
approval,
|
|
1080
|
+
respondToApproval,
|
|
1081
|
+
}) => {
|
|
1082
|
+
const isCancelled =
|
|
1083
|
+
status?.type === "incomplete" && status.reason === "cancelled";
|
|
1084
|
+
const isRequiresAction = status?.type === "requires-action";
|
|
1085
|
+
const shouldRenderApproval =
|
|
1086
|
+
isRequiresAction && offersInterruptAction(status, approval, interrupt);
|
|
1087
|
+
|
|
1088
|
+
const [open, setOpen] = useState(isRequiresAction);
|
|
1089
|
+
const [prevRequiresAction, setPrevRequiresAction] =
|
|
1090
|
+
useState(isRequiresAction);
|
|
1091
|
+
if (isRequiresAction !== prevRequiresAction) {
|
|
1092
|
+
setPrevRequiresAction(isRequiresAction);
|
|
1093
|
+
if (isRequiresAction) setOpen(true);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return (
|
|
1097
|
+
<ToolFallbackRoot open={open} onOpenChange={setOpen}>
|
|
1098
|
+
<ToolFallbackTrigger toolName={toolName} status={status} />
|
|
1099
|
+
<ToolFallbackContent>
|
|
1100
|
+
<ToolFallbackError status={status} />
|
|
1101
|
+
<ToolFallbackArgs
|
|
1102
|
+
argsText={argsText}
|
|
1103
|
+
className={cn(isCancelled && "opacity-60")}
|
|
1104
|
+
/>
|
|
1105
|
+
{(shouldRenderApproval || isSettled(approval)) && (
|
|
1106
|
+
<ToolFallbackApproval
|
|
1107
|
+
addResult={addResult}
|
|
1108
|
+
resume={resume}
|
|
1109
|
+
interrupt={interrupt}
|
|
1110
|
+
approval={approval}
|
|
1111
|
+
respondToApproval={respondToApproval}
|
|
1112
|
+
status={status}
|
|
1113
|
+
/>
|
|
1114
|
+
)}
|
|
1115
|
+
<ToolFallbackResult result={result} />
|
|
1116
|
+
</ToolFallbackContent>
|
|
1117
|
+
</ToolFallbackRoot>
|
|
1118
|
+
);
|
|
1119
|
+
};
|
|
1120
|
+
|
|
1121
|
+
const ToolFallback = memo(
|
|
1122
|
+
ToolFallbackImpl,
|
|
1123
|
+
) as unknown as ToolCallMessagePartComponent & {
|
|
1124
|
+
Root: typeof ToolFallbackRoot;
|
|
1125
|
+
Trigger: typeof ToolFallbackTrigger;
|
|
1126
|
+
Content: typeof ToolFallbackContent;
|
|
1127
|
+
Args: typeof ToolFallbackArgs;
|
|
1128
|
+
Result: typeof ToolFallbackResult;
|
|
1129
|
+
Error: typeof ToolFallbackError;
|
|
1130
|
+
Approval: typeof ToolFallbackApproval;
|
|
1131
|
+
};
|
|
1132
|
+
|
|
1133
|
+
ToolFallback.displayName = "ToolFallback";
|
|
1134
|
+
ToolFallback.Root = ToolFallbackRoot;
|
|
1135
|
+
ToolFallback.Trigger = ToolFallbackTrigger;
|
|
1136
|
+
ToolFallback.Content = ToolFallbackContent;
|
|
1137
|
+
ToolFallback.Args = ToolFallbackArgs;
|
|
1138
|
+
ToolFallback.Result = ToolFallbackResult;
|
|
1139
|
+
ToolFallback.Error = ToolFallbackError;
|
|
1140
|
+
ToolFallback.Approval = ToolFallbackApproval;
|
|
1141
|
+
|
|
1142
|
+
export {
|
|
1143
|
+
formatUnknownValue,
|
|
1144
|
+
offersInterruptAction,
|
|
1145
|
+
ToolFallback,
|
|
1146
|
+
ToolFallbackRoot,
|
|
1147
|
+
ToolFallbackTrigger,
|
|
1148
|
+
ToolFallbackContent,
|
|
1149
|
+
ToolFallbackArgs,
|
|
1150
|
+
ToolFallbackResult,
|
|
1151
|
+
ToolFallbackError,
|
|
1152
|
+
ToolFallbackApproval,
|
|
1153
|
+
};
|