assistant-ui 0.0.121 → 0.0.123
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/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +1 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +2 -2
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/info.js +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +95 -23
- package/dist/commands/mcp.js.map +1 -1
- package/dist/lib/agent-skill.d.ts +2 -2
- package/dist/lib/agent-skill.d.ts.map +1 -1
- package/dist/lib/agent-skill.js +10 -28
- package/dist/lib/agent-skill.js.map +1 -1
- package/dist/lib/create-project.d.ts +0 -9
- package/dist/lib/create-project.d.ts.map +1 -1
- package/dist/lib/create-project.js +15 -46
- package/dist/lib/create-project.js.map +1 -1
- package/dist/lib/transform.d.ts +0 -4
- package/dist/lib/transform.d.ts.map +1 -1
- package/dist/lib/transform.js +7 -16
- package/dist/lib/transform.js.map +1 -1
- package/dist/lib/utils/download.d.ts +5 -0
- package/dist/lib/utils/download.d.ts.map +1 -0
- package/dist/lib/utils/download.js +41 -0
- package/dist/lib/utils/download.js.map +1 -0
- package/dist/lib/utils/file-scanner.d.ts +0 -1
- package/dist/lib/utils/file-scanner.d.ts.map +1 -1
- package/dist/lib/utils/file-scanner.js +1 -16
- package/dist/lib/utils/file-scanner.js.map +1 -1
- package/dist/lib/utils/package-manager.d.ts +7 -0
- package/dist/lib/utils/package-manager.d.ts.map +1 -1
- package/dist/lib/utils/package-manager.js +15 -1
- package/dist/lib/utils/package-manager.js.map +1 -1
- package/package.json +3 -3
- package/src/commands/add.ts +2 -1
- package/src/commands/create.ts +5 -3
- package/src/commands/init.ts +5 -2
- package/src/commands/mcp.ts +223 -56
- package/src/lib/agent-skill.ts +18 -44
- package/src/lib/create-project.test.ts +0 -17
- package/src/lib/create-project.ts +33 -98
- package/src/lib/transform-files.test.ts +1 -3
- package/src/lib/transform.ts +6 -20
- package/src/lib/utils/download.ts +57 -0
- package/src/lib/utils/file-scanner.ts +0 -26
- package/src/lib/utils/package-manager.ts +26 -0
- package/src/lib/utils/registry.test.ts +0 -6
- package/templates/cloud-harness/app/api/chat/route.ts +1 -0
- package/templates/cloud-harness/components/assistant-ui/elements/collapsible-root.tsx +19 -0
- package/templates/cloud-harness/components/assistant-ui/elements/markdown-text.tsx +1 -1
- package/templates/cloud-harness/components/assistant-ui/elements/tool-fallback.aui.tsx +198 -30
- package/templates/cloud-harness/package.json +12 -12
package/src/lib/transform.ts
CHANGED
|
@@ -3,8 +3,8 @@ import path from "node:path";
|
|
|
3
3
|
import type { TransformOptions } from "./transform-options";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
|
-
import { sync as globSync } from "glob";
|
|
7
6
|
import { runSpawnCapture, SpawnExitError, SpawnSignalError } from "./run-spawn";
|
|
7
|
+
import { readProjectFiles } from "./utils/file-scanner";
|
|
8
8
|
|
|
9
9
|
const log = debug("codemod:transform");
|
|
10
10
|
|
|
@@ -22,7 +22,8 @@ export function getRelevantFiles(source: string): string[] {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const pattern = "**/*.{js,jsx,ts,tsx}";
|
|
25
|
-
const
|
|
25
|
+
const relevantFiles: string[] = [];
|
|
26
|
+
for (const { fullPath, content } of readProjectFiles(pattern, {
|
|
26
27
|
cwd: target,
|
|
27
28
|
ignore: [
|
|
28
29
|
"**/node_modules/**",
|
|
@@ -31,27 +32,12 @@ export function getRelevantFiles(source: string): string[] {
|
|
|
31
32
|
"**/*.min.js",
|
|
32
33
|
"**/*.bundle.js",
|
|
33
34
|
],
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
const content = fs.readFileSync(file, "utf8");
|
|
39
|
-
return content.includes("assistant-ui");
|
|
40
|
-
} catch {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
});
|
|
44
|
-
|
|
35
|
+
})) {
|
|
36
|
+
if (content.includes("assistant-ui")) relevantFiles.push(fullPath);
|
|
37
|
+
}
|
|
45
38
|
return relevantFiles;
|
|
46
39
|
}
|
|
47
40
|
|
|
48
|
-
/**
|
|
49
|
-
* Counts the number of files that need to be processed
|
|
50
|
-
*/
|
|
51
|
-
export function countFilesToProcess(source: string): number {
|
|
52
|
-
return getRelevantFiles(source).length;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
41
|
function buildCommand(
|
|
56
42
|
codemodPath: string,
|
|
57
43
|
targetFiles: string[],
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { downloadTemplate } from "giget";
|
|
2
|
+
|
|
3
|
+
export const DOWNLOAD_TIMEOUT_MS = 30_000;
|
|
4
|
+
|
|
5
|
+
export function resolveGitHubAuthToken(): string | undefined {
|
|
6
|
+
const token =
|
|
7
|
+
process.env.GIGET_AUTH ?? process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
8
|
+
const trimmed = token?.trim();
|
|
9
|
+
return trimmed || undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function withStagedDownload<T>(
|
|
13
|
+
source: string,
|
|
14
|
+
options: NonNullable<Parameters<typeof downloadTemplate>[1]>,
|
|
15
|
+
timeoutMessage: string,
|
|
16
|
+
cleanup: () => void | Promise<void>,
|
|
17
|
+
consumeDownload: (download: Promise<unknown>) => Promise<T>,
|
|
18
|
+
): Promise<T> {
|
|
19
|
+
// giget logs to console.debug whenever DEBUG is set, which the `debug`
|
|
20
|
+
// package does at module load for an unrelated namespace.
|
|
21
|
+
const origDebug = process.env.DEBUG;
|
|
22
|
+
delete process.env.DEBUG;
|
|
23
|
+
let download: Promise<unknown> | undefined;
|
|
24
|
+
let settled = false;
|
|
25
|
+
try {
|
|
26
|
+
const authToken = resolveGitHubAuthToken();
|
|
27
|
+
download = downloadTemplate(source, {
|
|
28
|
+
...options,
|
|
29
|
+
...(authToken ? { auth: authToken } : {}),
|
|
30
|
+
}).finally(() => {
|
|
31
|
+
settled = true;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
35
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
36
|
+
timer = setTimeout(
|
|
37
|
+
() => reject(new Error(timeoutMessage)),
|
|
38
|
+
DOWNLOAD_TIMEOUT_MS,
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
return await consumeDownload(Promise.race([download, timeout]));
|
|
43
|
+
} finally {
|
|
44
|
+
clearTimeout(timer!);
|
|
45
|
+
}
|
|
46
|
+
} finally {
|
|
47
|
+
try {
|
|
48
|
+
if (!download || settled) {
|
|
49
|
+
await cleanup();
|
|
50
|
+
} else {
|
|
51
|
+
void download.then(cleanup, cleanup);
|
|
52
|
+
}
|
|
53
|
+
} finally {
|
|
54
|
+
if (origDebug !== undefined) process.env.DEBUG = origDebug;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -49,29 +49,3 @@ export function scanForImport(
|
|
|
49
49
|
|
|
50
50
|
return false;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
export function getFilesContaining(
|
|
54
|
-
searchString: string,
|
|
55
|
-
options: ScanOptions = {},
|
|
56
|
-
): string[] {
|
|
57
|
-
const cwd = options.cwd || process.cwd();
|
|
58
|
-
const pattern = options.pattern || "**/*.{js,jsx,ts,tsx}";
|
|
59
|
-
const ignore = options.ignore || [
|
|
60
|
-
"**/node_modules/**",
|
|
61
|
-
"**/dist/**",
|
|
62
|
-
"**/build/**",
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
const result: string[] = [];
|
|
66
|
-
|
|
67
|
-
for (const { fullPath, content } of readProjectFiles(pattern, {
|
|
68
|
-
cwd,
|
|
69
|
-
ignore,
|
|
70
|
-
})) {
|
|
71
|
-
if (content.includes(searchString)) {
|
|
72
|
-
result.push(fullPath);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return result;
|
|
77
|
-
}
|
|
@@ -7,6 +7,32 @@ import { runSpawn, SpawnSignalError } from "../run-spawn";
|
|
|
7
7
|
|
|
8
8
|
export type PackageManagerName = "npm" | "pnpm" | "yarn" | "bun";
|
|
9
9
|
|
|
10
|
+
export function dlxCommand(pm: PackageManagerName): [string, string[]] {
|
|
11
|
+
switch (pm) {
|
|
12
|
+
case "pnpm":
|
|
13
|
+
return ["pnpm", ["dlx"]];
|
|
14
|
+
case "yarn":
|
|
15
|
+
return ["yarn", ["dlx"]];
|
|
16
|
+
case "bun":
|
|
17
|
+
return ["bunx", []];
|
|
18
|
+
case "npm":
|
|
19
|
+
return ["npx", ["--yes"]];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function resolvePackageManager(opts: {
|
|
24
|
+
useNpm?: boolean;
|
|
25
|
+
usePnpm?: boolean;
|
|
26
|
+
useYarn?: boolean;
|
|
27
|
+
useBun?: boolean;
|
|
28
|
+
}): PackageManagerName | undefined {
|
|
29
|
+
if (opts.useNpm) return "npm";
|
|
30
|
+
if (opts.usePnpm) return "pnpm";
|
|
31
|
+
if (opts.useYarn) return "yarn";
|
|
32
|
+
if (opts.useBun) return "bun";
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
10
36
|
export function askQuestion(query: string): Promise<string> {
|
|
11
37
|
return new Promise((resolve) => {
|
|
12
38
|
// A stream only reaches EOF once, so a run that already consumed stdin gets
|
|
@@ -144,12 +144,6 @@ describe("resolveRegistryItemUrl", () => {
|
|
|
144
144
|
);
|
|
145
145
|
});
|
|
146
146
|
|
|
147
|
-
it("uses the base URL for an explicit undefined style", () => {
|
|
148
|
-
expect(resolveRegistryItemUrl("thread", undefined)).toBe(
|
|
149
|
-
"https://r.assistant-ui.com/base/thread.json",
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
147
|
it("keeps the style inside a single path segment", () => {
|
|
154
148
|
expect(resolveRegistryItemUrl("thread", "base-nova?preview=1")).toBe(
|
|
155
149
|
"https://r.assistant-ui.com/styles/base-nova%3Fpreview%3D1/thread.json",
|
|
@@ -6,6 +6,7 @@ export const maxDuration = 30;
|
|
|
6
6
|
export const POST = async (request: Request) => {
|
|
7
7
|
const { messages } = (await request.json()) as { messages: UIMessage[] };
|
|
8
8
|
const result = streamText({
|
|
9
|
+
abortSignal: request.signal,
|
|
9
10
|
model: openai.responses("gpt-6-luna"),
|
|
10
11
|
messages: await convertToModelMessages(messages),
|
|
11
12
|
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Collapsible } from "@base-ui/react/collapsible";
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
|
|
6
|
+
export const CollapsibleRoot = forwardRef<
|
|
7
|
+
React.ComponentRef<typeof Collapsible.Root>,
|
|
8
|
+
React.ComponentPropsWithoutRef<typeof Collapsible.Root>
|
|
9
|
+
>(function CollapsibleRoot(props, ref) {
|
|
10
|
+
return <Collapsible.Root ref={ref} data-slot="collapsible" {...props} />;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export function CollapsibleTrigger({ ...props }: Collapsible.Trigger.Props) {
|
|
14
|
+
return <Collapsible.Trigger data-slot="collapsible-trigger" {...props} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CollapsibleContent({ ...props }: Collapsible.Panel.Props) {
|
|
18
|
+
return <Collapsible.Panel data-slot="collapsible-content" {...props} />;
|
|
19
|
+
}
|
|
@@ -5,7 +5,7 @@ import "@assistant-ui/react-markdown/styles/dot.css";
|
|
|
5
5
|
import {
|
|
6
6
|
type CodeHeaderProps,
|
|
7
7
|
MarkdownTextPrimitive,
|
|
8
|
-
|
|
8
|
+
memoizeMarkdownComponents,
|
|
9
9
|
useIsMarkdownCodeBlock,
|
|
10
10
|
} from "@assistant-ui/react-markdown";
|
|
11
11
|
import remarkGfm from "remark-gfm";
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
forwardRef,
|
|
5
|
+
memo,
|
|
6
|
+
useCallback,
|
|
7
|
+
useImperativeHandle,
|
|
8
|
+
useLayoutEffect,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from "react";
|
|
4
12
|
import {
|
|
5
13
|
AlertCircleIcon,
|
|
6
14
|
CheckIcon,
|
|
@@ -22,12 +30,12 @@ import {
|
|
|
22
30
|
type ToolCallMessagePartStatus,
|
|
23
31
|
type ToolCallMessagePartComponent,
|
|
24
32
|
} from "@assistant-ui/react";
|
|
33
|
+
import { cn } from "@/lib/utils";
|
|
25
34
|
import {
|
|
26
|
-
Collapsible,
|
|
35
|
+
CollapsibleRoot as Collapsible,
|
|
27
36
|
CollapsibleContent,
|
|
28
37
|
CollapsibleTrigger,
|
|
29
|
-
} from "
|
|
30
|
-
import { cn } from "@/lib/utils";
|
|
38
|
+
} from "./collapsible-root";
|
|
31
39
|
import { Button } from "@/components/ui/button";
|
|
32
40
|
import { Textarea } from "@/components/ui/textarea";
|
|
33
41
|
|
|
@@ -158,7 +166,7 @@ function ToolFallbackTrigger({
|
|
|
158
166
|
<CollapsibleTrigger
|
|
159
167
|
data-slot="tool-fallback-trigger"
|
|
160
168
|
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]",
|
|
169
|
+
"aui-tool-fallback-trigger group/trigger text-muted-foreground hover:text-foreground flex w-fit max-w-full origin-left items-center gap-2 py-1.5 text-sm transition-[color,scale] active:scale-[0.98]",
|
|
162
170
|
className,
|
|
163
171
|
)}
|
|
164
172
|
{...props}
|
|
@@ -174,7 +182,7 @@ function ToolFallbackTrigger({
|
|
|
174
182
|
<span
|
|
175
183
|
data-slot="tool-fallback-trigger-label"
|
|
176
184
|
className={cn(
|
|
177
|
-
"aui-tool-fallback-trigger-label-wrapper inline-block text-start leading-none",
|
|
185
|
+
"aui-tool-fallback-trigger-label-wrapper inline-block min-w-0 text-start leading-none wrap-anywhere",
|
|
178
186
|
isCancelled && "text-muted-foreground line-through",
|
|
179
187
|
isRunning && "shimmer motion-reduce:animate-none",
|
|
180
188
|
)}
|
|
@@ -245,7 +253,7 @@ function ToolFallbackArgs({
|
|
|
245
253
|
className={cn("aui-tool-fallback-args", className)}
|
|
246
254
|
{...props}
|
|
247
255
|
>
|
|
248
|
-
<pre className="aui-tool-fallback-args-value bg-muted/50 text-foreground/90 rounded-md p-2.5 text-xs whitespace-pre-wrap">
|
|
256
|
+
<pre className="aui-tool-fallback-args-value bg-muted/50 text-foreground/90 rounded-md p-2.5 text-xs break-words whitespace-pre-wrap">
|
|
249
257
|
{argsText}
|
|
250
258
|
</pre>
|
|
251
259
|
</div>
|
|
@@ -287,7 +295,7 @@ function ToolFallbackResult({
|
|
|
287
295
|
<p className="aui-tool-fallback-result-header text-muted-foreground text-xs font-medium">
|
|
288
296
|
Result:
|
|
289
297
|
</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">
|
|
298
|
+
<pre className="aui-tool-fallback-result-content bg-muted/50 text-foreground/90 mt-1 rounded-md p-2.5 text-xs break-words whitespace-pre-wrap">
|
|
291
299
|
{formatUnknownValue(result, 2)}
|
|
292
300
|
</pre>
|
|
293
301
|
</div>
|
|
@@ -321,7 +329,7 @@ function ToolFallbackError({
|
|
|
321
329
|
<p className="aui-tool-fallback-error-header text-muted-foreground font-semibold">
|
|
322
330
|
{headerText}
|
|
323
331
|
</p>
|
|
324
|
-
<p className="aui-tool-fallback-error-reason text-muted-foreground whitespace-pre-line">
|
|
332
|
+
<p className="aui-tool-fallback-error-reason text-muted-foreground break-words whitespace-pre-line">
|
|
325
333
|
{errorText}
|
|
326
334
|
</p>
|
|
327
335
|
</div>
|
|
@@ -436,10 +444,20 @@ const receiptIcons = {
|
|
|
436
444
|
function ToolFallbackApprovalReceipt({
|
|
437
445
|
approval,
|
|
438
446
|
className,
|
|
447
|
+
focusReceiptRef,
|
|
448
|
+
receiptRef,
|
|
439
449
|
...props
|
|
440
|
-
}: React.
|
|
450
|
+
}: React.ComponentPropsWithoutRef<"div"> & {
|
|
441
451
|
approval: NonNullable<ToolCallMessagePart["approval"]>;
|
|
452
|
+
focusReceiptRef: React.MutableRefObject<string | null | undefined>;
|
|
453
|
+
receiptRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
442
454
|
}) {
|
|
455
|
+
useLayoutEffect(() => {
|
|
456
|
+
const shouldFocus = focusReceiptRef.current === approval.id;
|
|
457
|
+
focusReceiptRef.current = null;
|
|
458
|
+
if (shouldFocus) receiptRef.current?.focus({ preventScroll: true });
|
|
459
|
+
}, [approval.id, focusReceiptRef, receiptRef]);
|
|
460
|
+
|
|
443
461
|
const receipt = approvalReceipt(approval);
|
|
444
462
|
const Icon = receiptIcons[receipt.outcome];
|
|
445
463
|
const notes = [
|
|
@@ -452,8 +470,10 @@ function ToolFallbackApprovalReceipt({
|
|
|
452
470
|
|
|
453
471
|
return (
|
|
454
472
|
<div
|
|
473
|
+
ref={receiptRef}
|
|
455
474
|
data-slot="tool-fallback-approval-receipt"
|
|
456
475
|
data-outcome={receipt.outcome}
|
|
476
|
+
tabIndex={-1}
|
|
457
477
|
className={cn(
|
|
458
478
|
"aui-tool-fallback-approval-receipt flex flex-col gap-1.5 pt-1",
|
|
459
479
|
className,
|
|
@@ -675,14 +695,36 @@ const offersInterruptAction = (
|
|
|
675
695
|
approval != null ||
|
|
676
696
|
interrupt != null;
|
|
677
697
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
698
|
+
type ToolFallbackApprovalProps = React.ComponentPropsWithoutRef<"div"> &
|
|
699
|
+
Partial<
|
|
700
|
+
Pick<
|
|
701
|
+
ToolCallMessagePartProps,
|
|
702
|
+
"addResult" | "resume" | "respondToApproval" | "status"
|
|
703
|
+
>
|
|
704
|
+
> & {
|
|
705
|
+
interrupt?: ToolCallMessagePart["interrupt"];
|
|
706
|
+
approval?: ToolCallMessagePart["approval"];
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const ToolFallbackApproval = forwardRef<
|
|
710
|
+
HTMLDivElement,
|
|
711
|
+
ToolFallbackApprovalProps
|
|
712
|
+
>(function ToolFallbackApproval(props, ref) {
|
|
713
|
+
const carryFocusRef = useRef(false);
|
|
714
|
+
return (
|
|
715
|
+
<ToolFallbackApprovalImpl
|
|
716
|
+
key={props.approval?.id}
|
|
717
|
+
carryFocusRef={carryFocusRef}
|
|
718
|
+
forwardedRef={ref}
|
|
719
|
+
{...props}
|
|
720
|
+
/>
|
|
721
|
+
);
|
|
722
|
+
});
|
|
683
723
|
|
|
684
724
|
function ToolFallbackApprovalImpl({
|
|
685
725
|
className,
|
|
726
|
+
forwardedRef,
|
|
727
|
+
carryFocusRef,
|
|
686
728
|
addResult,
|
|
687
729
|
resume,
|
|
688
730
|
interrupt,
|
|
@@ -690,16 +732,10 @@ function ToolFallbackApprovalImpl({
|
|
|
690
732
|
respondToApproval,
|
|
691
733
|
status,
|
|
692
734
|
...props
|
|
693
|
-
}:
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
"addResult" | "resume" | "respondToApproval" | "status"
|
|
698
|
-
>
|
|
699
|
-
> & {
|
|
700
|
-
interrupt?: ToolCallMessagePart["interrupt"];
|
|
701
|
-
approval?: ToolCallMessagePart["approval"];
|
|
702
|
-
}) {
|
|
735
|
+
}: Omit<ToolFallbackApprovalProps, "ref"> & {
|
|
736
|
+
forwardedRef: React.ForwardedRef<HTMLDivElement>;
|
|
737
|
+
carryFocusRef: React.MutableRefObject<boolean>;
|
|
738
|
+
}) {
|
|
703
739
|
const [submitted, setSubmitted] = useState(false);
|
|
704
740
|
const voiceActive = useAuiState((s) => s.thread.voice !== undefined);
|
|
705
741
|
const canAnswer = useAuiState((s) => s.thread.capabilities.answerToolCall);
|
|
@@ -707,17 +743,85 @@ function ToolFallbackApprovalImpl({
|
|
|
707
743
|
const [confirmingId, setConfirmingId] = useState<string | null>(null);
|
|
708
744
|
const [answer, setAnswer] = useState("");
|
|
709
745
|
const [error, setError] = useState<string | null>(null);
|
|
746
|
+
const isRequiresAction = status?.type === "requires-action";
|
|
747
|
+
const [prevRequiresAction, setPrevRequiresAction] =
|
|
748
|
+
useState(isRequiresAction);
|
|
749
|
+
const [attempt, setAttempt] = useState(0);
|
|
750
|
+
const attemptRef = useRef(attempt);
|
|
751
|
+
useLayoutEffect(() => {
|
|
752
|
+
attemptRef.current = attempt;
|
|
753
|
+
}, [attempt]);
|
|
754
|
+
if (isRequiresAction !== prevRequiresAction) {
|
|
755
|
+
setPrevRequiresAction(isRequiresAction);
|
|
756
|
+
if (isRequiresAction) {
|
|
757
|
+
setSubmitted(false);
|
|
758
|
+
setConfirmingId(null);
|
|
759
|
+
setAnswer("");
|
|
760
|
+
setError(null);
|
|
761
|
+
setAttempt(attempt + 1);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
const pendingGroupRef = useRef<HTMLDivElement | null>(null);
|
|
765
|
+
const receiptRef = useRef<HTMLDivElement | null>(null);
|
|
766
|
+
const focusReceiptRef = useRef<string | null | undefined>(null);
|
|
767
|
+
useLayoutEffect(() => {
|
|
768
|
+
if (carryFocusRef.current) {
|
|
769
|
+
carryFocusRef.current = false;
|
|
770
|
+
pendingGroupRef.current?.focus({ preventScroll: true });
|
|
771
|
+
}
|
|
772
|
+
return () => {
|
|
773
|
+
const element = pendingGroupRef.current ?? receiptRef.current;
|
|
774
|
+
carryFocusRef.current =
|
|
775
|
+
element?.contains(element.ownerDocument.activeElement) ?? false;
|
|
776
|
+
};
|
|
777
|
+
}, [carryFocusRef]);
|
|
778
|
+
useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(
|
|
779
|
+
forwardedRef,
|
|
780
|
+
() => pendingGroupRef.current ?? receiptRef.current,
|
|
781
|
+
);
|
|
782
|
+
const setPendingGroup = useCallback(
|
|
783
|
+
(element: HTMLDivElement | null) => {
|
|
784
|
+
const group = pendingGroupRef.current;
|
|
785
|
+
focusReceiptRef.current =
|
|
786
|
+
!element && group?.contains(group.ownerDocument.activeElement)
|
|
787
|
+
? approval?.id
|
|
788
|
+
: null;
|
|
789
|
+
pendingGroupRef.current = element;
|
|
790
|
+
},
|
|
791
|
+
[approval?.id],
|
|
792
|
+
);
|
|
793
|
+
const hidePendingApproval =
|
|
794
|
+
approval != null &&
|
|
795
|
+
!isSettled(approval) &&
|
|
796
|
+
status !== undefined &&
|
|
797
|
+
status.type !== "requires-action" &&
|
|
798
|
+
!(submitted && status.type === "running");
|
|
799
|
+
useLayoutEffect(() => {
|
|
800
|
+
if (hidePendingApproval) focusReceiptRef.current = null;
|
|
801
|
+
}, [hidePendingApproval]);
|
|
802
|
+
const focusPendingGroup = () => {
|
|
803
|
+
const group = pendingGroupRef.current;
|
|
804
|
+
if (group?.contains(group.ownerDocument.activeElement)) {
|
|
805
|
+
group.focus({ preventScroll: true });
|
|
806
|
+
}
|
|
807
|
+
};
|
|
710
808
|
|
|
711
809
|
if (approval != null && isSettled(approval))
|
|
712
810
|
return (
|
|
713
811
|
<ToolFallbackApprovalReceipt
|
|
714
812
|
approval={approval}
|
|
813
|
+
receiptRef={receiptRef}
|
|
814
|
+
focusReceiptRef={focusReceiptRef}
|
|
715
815
|
className={className}
|
|
716
816
|
{...props}
|
|
717
817
|
/>
|
|
718
818
|
);
|
|
719
819
|
|
|
720
|
-
if (
|
|
820
|
+
if (
|
|
821
|
+
hidePendingApproval ||
|
|
822
|
+
!offersInterruptAction(status, approval, interrupt)
|
|
823
|
+
)
|
|
824
|
+
return null;
|
|
721
825
|
|
|
722
826
|
const promptText = approval?.prompt ? (
|
|
723
827
|
<p className="aui-tool-fallback-approval-prompt text-foreground whitespace-pre-line">
|
|
@@ -730,11 +834,13 @@ function ToolFallbackApprovalImpl({
|
|
|
730
834
|
promptText && (
|
|
731
835
|
<div
|
|
732
836
|
data-slot="tool-fallback-approval"
|
|
837
|
+
tabIndex={-1}
|
|
733
838
|
className={cn(
|
|
734
839
|
"aui-tool-fallback-approval flex flex-col gap-2 pt-1",
|
|
735
840
|
className,
|
|
736
841
|
)}
|
|
737
842
|
{...props}
|
|
843
|
+
ref={setPendingGroup}
|
|
738
844
|
>
|
|
739
845
|
{promptText}
|
|
740
846
|
</div>
|
|
@@ -753,12 +859,15 @@ function ToolFallbackApprovalImpl({
|
|
|
753
859
|
// A refused response leaves the request open, so the controls come back
|
|
754
860
|
// rather than staying spent on a decision the runtime never recorded.
|
|
755
861
|
const submit = (send: () => Promise<void> | void) => {
|
|
862
|
+
focusPendingGroup();
|
|
756
863
|
setSubmitted(true);
|
|
864
|
+
setConfirmingId(null);
|
|
757
865
|
setError(null);
|
|
758
866
|
void (async () => {
|
|
759
867
|
try {
|
|
760
868
|
await send();
|
|
761
869
|
} catch (sendError) {
|
|
870
|
+
if (attemptRef.current !== attempt) return;
|
|
762
871
|
setSubmitted(false);
|
|
763
872
|
setError(
|
|
764
873
|
sendError instanceof Error ? sendError.message : String(sendError),
|
|
@@ -789,7 +898,6 @@ function ToolFallbackApprovalImpl({
|
|
|
789
898
|
|
|
790
899
|
const respondWithOption = (option: ToolApprovalOption) => {
|
|
791
900
|
if (locked) return;
|
|
792
|
-
setConfirmingId(null);
|
|
793
901
|
// A custom kind has no decision class for the runtime to derive, and
|
|
794
902
|
// responding without one throws; picking a declared option is an answer,
|
|
795
903
|
// so it resolves as approved.
|
|
@@ -819,6 +927,7 @@ function ToolFallbackApprovalImpl({
|
|
|
819
927
|
|
|
820
928
|
const handleOption = (option: ToolApprovalOption) => {
|
|
821
929
|
if (option.confirm) {
|
|
930
|
+
focusPendingGroup();
|
|
822
931
|
setConfirmingId(option.id);
|
|
823
932
|
} else {
|
|
824
933
|
respondWithOption(option);
|
|
@@ -886,11 +995,13 @@ function ToolFallbackApprovalImpl({
|
|
|
886
995
|
return (
|
|
887
996
|
<div
|
|
888
997
|
data-slot="tool-fallback-approval"
|
|
998
|
+
tabIndex={-1}
|
|
889
999
|
className={cn(
|
|
890
1000
|
"aui-tool-fallback-approval flex flex-col gap-3 pt-1",
|
|
891
1001
|
className,
|
|
892
1002
|
)}
|
|
893
1003
|
{...props}
|
|
1004
|
+
ref={setPendingGroup}
|
|
894
1005
|
>
|
|
895
1006
|
{promptText}
|
|
896
1007
|
<ToolFallbackApprovalQuestions
|
|
@@ -913,11 +1024,13 @@ function ToolFallbackApprovalImpl({
|
|
|
913
1024
|
return (
|
|
914
1025
|
<div
|
|
915
1026
|
data-slot="tool-fallback-approval-confirm"
|
|
1027
|
+
tabIndex={-1}
|
|
916
1028
|
className={cn(
|
|
917
1029
|
"aui-tool-fallback-approval-confirm flex flex-col gap-2 pt-1",
|
|
918
1030
|
className,
|
|
919
1031
|
)}
|
|
920
1032
|
{...props}
|
|
1033
|
+
ref={setPendingGroup}
|
|
921
1034
|
>
|
|
922
1035
|
<p className="aui-tool-fallback-approval-confirm-title font-semibold">
|
|
923
1036
|
{confirmMeta?.title ?? `${approvalOptionLabel(confirming)}?`}
|
|
@@ -951,7 +1064,10 @@ function ToolFallbackApprovalImpl({
|
|
|
951
1064
|
size="sm"
|
|
952
1065
|
variant="outline"
|
|
953
1066
|
className={pressable}
|
|
954
|
-
onClick={() =>
|
|
1067
|
+
onClick={() => {
|
|
1068
|
+
focusPendingGroup();
|
|
1069
|
+
setConfirmingId(null);
|
|
1070
|
+
}}
|
|
955
1071
|
disabled={locked}
|
|
956
1072
|
>
|
|
957
1073
|
Back
|
|
@@ -970,11 +1086,13 @@ function ToolFallbackApprovalImpl({
|
|
|
970
1086
|
return (
|
|
971
1087
|
<div
|
|
972
1088
|
data-slot="tool-fallback-approval"
|
|
1089
|
+
tabIndex={-1}
|
|
973
1090
|
className={cn(
|
|
974
1091
|
"aui-tool-fallback-approval flex flex-col gap-2 pt-1",
|
|
975
1092
|
className,
|
|
976
1093
|
)}
|
|
977
1094
|
{...props}
|
|
1095
|
+
ref={setPendingGroup}
|
|
978
1096
|
>
|
|
979
1097
|
{promptText}
|
|
980
1098
|
<div className="flex flex-wrap items-center gap-2">
|
|
@@ -1017,11 +1135,13 @@ function ToolFallbackApprovalImpl({
|
|
|
1017
1135
|
return (
|
|
1018
1136
|
<div
|
|
1019
1137
|
data-slot="tool-fallback-approval"
|
|
1138
|
+
tabIndex={-1}
|
|
1020
1139
|
className={cn(
|
|
1021
1140
|
"aui-tool-fallback-approval flex flex-col gap-2 pt-1",
|
|
1022
1141
|
className,
|
|
1023
1142
|
)}
|
|
1024
1143
|
{...props}
|
|
1144
|
+
ref={setPendingGroup}
|
|
1025
1145
|
>
|
|
1026
1146
|
{promptText}
|
|
1027
1147
|
{answerField}
|
|
@@ -1036,11 +1156,13 @@ function ToolFallbackApprovalImpl({
|
|
|
1036
1156
|
return (
|
|
1037
1157
|
<div
|
|
1038
1158
|
data-slot="tool-fallback-approval"
|
|
1159
|
+
tabIndex={-1}
|
|
1039
1160
|
className={cn(
|
|
1040
1161
|
"aui-tool-fallback-approval flex flex-col gap-2 pt-1",
|
|
1041
1162
|
className,
|
|
1042
1163
|
)}
|
|
1043
1164
|
{...props}
|
|
1165
|
+
ref={setPendingGroup}
|
|
1044
1166
|
>
|
|
1045
1167
|
{promptText}
|
|
1046
1168
|
<div className="flex items-center gap-2">
|
|
@@ -1083,7 +1205,8 @@ const ToolFallbackImpl: ToolCallMessagePartComponent = ({
|
|
|
1083
1205
|
status?.type === "incomplete" && status.reason === "cancelled";
|
|
1084
1206
|
const isRequiresAction = status?.type === "requires-action";
|
|
1085
1207
|
const shouldRenderApproval =
|
|
1086
|
-
|
|
1208
|
+
approval != null ||
|
|
1209
|
+
(isRequiresAction && offersInterruptAction(status, approval, interrupt));
|
|
1087
1210
|
|
|
1088
1211
|
const [open, setOpen] = useState(isRequiresAction);
|
|
1089
1212
|
const [prevRequiresAction, setPrevRequiresAction] =
|
|
@@ -1092,6 +1215,31 @@ const ToolFallbackImpl: ToolCallMessagePartComponent = ({
|
|
|
1092
1215
|
setPrevRequiresAction(isRequiresAction);
|
|
1093
1216
|
if (isRequiresAction) setOpen(true);
|
|
1094
1217
|
}
|
|
1218
|
+
const approvalHadFocusRef = useRef(false);
|
|
1219
|
+
const approvalElementRef = useRef<HTMLDivElement | null>(null);
|
|
1220
|
+
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
|
1221
|
+
|
|
1222
|
+
useLayoutEffect(() => {
|
|
1223
|
+
const focusedElement = approvalElementRef.current;
|
|
1224
|
+
if (
|
|
1225
|
+
!focusedElement ||
|
|
1226
|
+
focusedElement.isConnected ||
|
|
1227
|
+
isRequiresAction ||
|
|
1228
|
+
!approvalHadFocusRef.current
|
|
1229
|
+
) {
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
const shouldFocusTrigger = approval == null || !isSettled(approval);
|
|
1234
|
+
approvalHadFocusRef.current = false;
|
|
1235
|
+
const { activeElement, body } = focusedElement.ownerDocument;
|
|
1236
|
+
if (
|
|
1237
|
+
shouldFocusTrigger &&
|
|
1238
|
+
(activeElement == null || activeElement === body)
|
|
1239
|
+
) {
|
|
1240
|
+
triggerRef.current?.focus({ preventScroll: true });
|
|
1241
|
+
}
|
|
1242
|
+
}, [approval, isRequiresAction, status?.type]);
|
|
1095
1243
|
|
|
1096
1244
|
return (
|
|
1097
1245
|
<ToolFallbackRoot open={open} onOpenChange={setOpen}>
|
|
@@ -1102,7 +1250,7 @@ const ToolFallbackImpl: ToolCallMessagePartComponent = ({
|
|
|
1102
1250
|
argsText={argsText}
|
|
1103
1251
|
className={cn(isCancelled && "opacity-60")}
|
|
1104
1252
|
/>
|
|
1105
|
-
{
|
|
1253
|
+
{shouldRenderApproval && (
|
|
1106
1254
|
<ToolFallbackApproval
|
|
1107
1255
|
addResult={addResult}
|
|
1108
1256
|
resume={resume}
|
|
@@ -1110,6 +1258,26 @@ const ToolFallbackImpl: ToolCallMessagePartComponent = ({
|
|
|
1110
1258
|
approval={approval}
|
|
1111
1259
|
respondToApproval={respondToApproval}
|
|
1112
1260
|
status={status}
|
|
1261
|
+
onFocusCapture={(event) => {
|
|
1262
|
+
const root = event.currentTarget.closest(
|
|
1263
|
+
'[data-slot="tool-fallback-root"]',
|
|
1264
|
+
);
|
|
1265
|
+
triggerRef.current =
|
|
1266
|
+
root?.querySelector<HTMLButtonElement>(
|
|
1267
|
+
'[data-slot="tool-fallback-trigger"]',
|
|
1268
|
+
) ?? null;
|
|
1269
|
+
approvalElementRef.current = event.currentTarget;
|
|
1270
|
+
approvalHadFocusRef.current = true;
|
|
1271
|
+
}}
|
|
1272
|
+
onBlurCapture={(event) => {
|
|
1273
|
+
const relatedTarget = event.relatedTarget;
|
|
1274
|
+
const stillFocused = relatedTarget
|
|
1275
|
+
? event.currentTarget.contains(relatedTarget as Node)
|
|
1276
|
+
: event.currentTarget.contains(
|
|
1277
|
+
event.currentTarget.ownerDocument.activeElement,
|
|
1278
|
+
);
|
|
1279
|
+
if (!stillFocused) approvalHadFocusRef.current = false;
|
|
1280
|
+
}}
|
|
1113
1281
|
/>
|
|
1114
1282
|
)}
|
|
1115
1283
|
<ToolFallbackResult result={result} />
|