@truefoundry/agent-ui-sdk 0.0.1 → 0.0.2

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.
Files changed (79) hide show
  1. package/README.md +192 -80
  2. package/dist/index.d.ts +228 -281
  3. package/dist/index.js +1823 -1791
  4. package/dist/index.js.map +1 -1
  5. package/dist/styles.css +2 -0
  6. package/package.json +36 -19
  7. package/src/atoms/AskUserPrompt.tsx +0 -102
  8. package/src/atoms/AttachmentCard.tsx +0 -92
  9. package/src/atoms/AttachmentPickerButton.tsx +0 -19
  10. package/src/atoms/AttachmentPreviewDialog.tsx +0 -34
  11. package/src/atoms/BranchIndicator.tsx +0 -41
  12. package/src/atoms/CodeBlockHeader.tsx +0 -44
  13. package/src/atoms/ComposerShell.tsx +0 -112
  14. package/src/atoms/Markdown.test.tsx +0 -28
  15. package/src/atoms/Markdown.tsx +0 -288
  16. package/src/atoms/McpAuthPrompt.tsx +0 -60
  17. package/src/atoms/MessageActionBar.tsx +0 -60
  18. package/src/atoms/MessageBubble.tsx +0 -80
  19. package/src/atoms/MessageErrorBanner.tsx +0 -26
  20. package/src/atoms/MessageIndicator.tsx +0 -23
  21. package/src/atoms/OpenUIBlock.test.tsx +0 -37
  22. package/src/atoms/OpenUIBlock.tsx +0 -50
  23. package/src/atoms/ReasoningCard.tsx +0 -52
  24. package/src/atoms/SandboxArtifactList.tsx +0 -63
  25. package/src/atoms/ScrollToBottomButton.tsx +0 -34
  26. package/src/atoms/Skeletons.tsx +0 -32
  27. package/src/atoms/ThreadListMisc.tsx +0 -76
  28. package/src/atoms/ThreadListRow.tsx +0 -81
  29. package/src/atoms/ThreadShell.tsx +0 -93
  30. package/src/atoms/Toast.tsx +0 -60
  31. package/src/atoms/ToolApprovalBar.tsx +0 -92
  32. package/src/atoms/ToolCallCard.tsx +0 -186
  33. package/src/atoms/ToolGroupCard.tsx +0 -52
  34. package/src/atoms/UserMessageActionBar.tsx +0 -62
  35. package/src/atoms/WelcomeScreen.tsx +0 -22
  36. package/src/atoms/lib/cn.ts +0 -6
  37. package/src/atoms/primitives/Avatar.tsx +0 -57
  38. package/src/atoms/primitives/Button.tsx +0 -73
  39. package/src/atoms/primitives/Collapsible.tsx +0 -32
  40. package/src/atoms/primitives/Dialog.tsx +0 -152
  41. package/src/atoms/primitives/IconButton.tsx +0 -43
  42. package/src/atoms/primitives/Skeleton.tsx +0 -17
  43. package/src/atoms/primitives/Tooltip.tsx +0 -58
  44. package/src/containers/AskUserContainer.tsx +0 -49
  45. package/src/containers/AssistantMessageContainer.test.tsx +0 -56
  46. package/src/containers/AssistantMessageContainer.tsx +0 -128
  47. package/src/containers/AssistantTextContainer.test.tsx +0 -111
  48. package/src/containers/AssistantTextContainer.tsx +0 -54
  49. package/src/containers/AttachmentsContainer.tsx +0 -83
  50. package/src/containers/ComposerContainer.tsx +0 -66
  51. package/src/containers/ErrorToasterContainer.tsx +0 -76
  52. package/src/containers/McpAuthContainer.test.tsx +0 -100
  53. package/src/containers/McpAuthContainer.tsx +0 -22
  54. package/src/containers/MessageImageContainer.tsx +0 -37
  55. package/src/containers/ReasoningContainer.tsx +0 -32
  56. package/src/containers/RuntimeHarness.tsx +0 -39
  57. package/src/containers/Thread.tsx +0 -9
  58. package/src/containers/ThreadContainer.test.tsx +0 -52
  59. package/src/containers/ThreadContainer.tsx +0 -87
  60. package/src/containers/ThreadListContainer.tsx +0 -63
  61. package/src/containers/ToolCallContainer.test.tsx +0 -135
  62. package/src/containers/ToolCallContainer.tsx +0 -167
  63. package/src/containers/ToolGroupContainer.test.tsx +0 -56
  64. package/src/containers/ToolGroupContainer.tsx +0 -24
  65. package/src/containers/UserEditComposerContainer.test.tsx +0 -52
  66. package/src/containers/UserEditComposerContainer.tsx +0 -86
  67. package/src/containers/UserMessageContainer.test.tsx +0 -99
  68. package/src/containers/UserMessageContainer.tsx +0 -26
  69. package/src/containers/nestedApprovalBridge.ts +0 -20
  70. package/src/containers/useAttachmentPreviewSrc.ts +0 -42
  71. package/src/hooks/useComposerBusyState.test.ts +0 -78
  72. package/src/hooks/useComposerBusyState.ts +0 -69
  73. package/src/index.ts +0 -193
  74. package/src/testSetup.ts +0 -42
  75. package/src/theme/SlotsProvider.test.tsx +0 -64
  76. package/src/theme/SlotsProvider.tsx +0 -41
  77. package/src/theme/defaultSlots.ts +0 -131
  78. package/src/theme/tokens.ts +0 -98
  79. package/src/theme/useClassDarkMode.ts +0 -24
@@ -1,43 +0,0 @@
1
- import { forwardRef, type ComponentPropsWithRef } from "react";
2
- import { Slot } from "radix-ui";
3
-
4
- import { cn } from "../lib/cn.js";
5
- import { Button } from "./Button.js";
6
- import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./Tooltip.js";
7
-
8
- export type IconButtonProps = ComponentPropsWithRef<typeof Button> & {
9
- tooltip: string;
10
- side?: "top" | "bottom" | "left" | "right";
11
- };
12
-
13
- export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(
14
- ({ children, tooltip, side = "bottom", className, ...rest }, ref) => {
15
- return (
16
- <TooltipProvider delayDuration={0}>
17
- <Tooltip>
18
- <TooltipTrigger asChild>
19
- <Button
20
- variant="ghost"
21
- size="icon"
22
- {...rest}
23
- className={cn("aui-button-icon size-6 p-1 active:scale-90", className)}
24
- ref={ref}
25
- >
26
- <Slot.Slottable>{children}</Slot.Slottable>
27
- <span className="aui-sr-only sr-only">{tooltip}</span>
28
- </Button>
29
- </TooltipTrigger>
30
- <TooltipContent side={side}>{tooltip}</TooltipContent>
31
- </Tooltip>
32
- </TooltipProvider>
33
- );
34
- },
35
- );
36
-
37
- IconButton.displayName = "IconButton";
38
-
39
- declare module "../../theme/SlotsProvider.js" {
40
- interface AtomSlots {
41
- IconButton: typeof IconButton;
42
- }
43
- }
@@ -1,17 +0,0 @@
1
- import type { ComponentProps } from "react";
2
-
3
- import { cn } from "../lib/cn.js";
4
-
5
- export type SkeletonProps = ComponentProps<"div">;
6
-
7
- export function Skeleton({ className, ...props }: SkeletonProps) {
8
- return (
9
- <div data-slot="skeleton" className={cn("animate-pulse rounded-md bg-muted", className)} {...props} />
10
- );
11
- }
12
-
13
- declare module "../../theme/SlotsProvider.js" {
14
- interface AtomSlots {
15
- Skeleton: typeof Skeleton;
16
- }
17
- }
@@ -1,58 +0,0 @@
1
- import type { ComponentProps } from "react";
2
- import { Tooltip as TooltipPrimitive } from "radix-ui";
3
-
4
- import { cn } from "../lib/cn.js";
5
-
6
- export type TooltipProviderProps = ComponentProps<typeof TooltipPrimitive.Provider>;
7
-
8
- export function TooltipProvider({ delayDuration = 0, ...props }: TooltipProviderProps) {
9
- return (
10
- <TooltipPrimitive.Provider
11
- data-slot="tooltip-provider"
12
- delayDuration={delayDuration}
13
- {...props}
14
- />
15
- );
16
- }
17
-
18
- export type TooltipProps = ComponentProps<typeof TooltipPrimitive.Root>;
19
-
20
- export function Tooltip(props: TooltipProps) {
21
- return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
22
- }
23
-
24
- export type TooltipTriggerProps = ComponentProps<typeof TooltipPrimitive.Trigger>;
25
-
26
- export function TooltipTrigger(props: TooltipTriggerProps) {
27
- return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
28
- }
29
-
30
- export type TooltipContentProps = ComponentProps<typeof TooltipPrimitive.Content>;
31
-
32
- export function TooltipContent({ className, sideOffset = 0, children, ...props }: TooltipContentProps) {
33
- return (
34
- <TooltipPrimitive.Portal>
35
- <TooltipPrimitive.Content
36
- data-slot="tooltip-content"
37
- sideOffset={sideOffset}
38
- className={cn(
39
- "z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
40
- className,
41
- )}
42
- {...props}
43
- >
44
- {children}
45
- <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
46
- </TooltipPrimitive.Content>
47
- </TooltipPrimitive.Portal>
48
- );
49
- }
50
-
51
- declare module "../../theme/SlotsProvider.js" {
52
- interface AtomSlots {
53
- TooltipProvider: typeof TooltipProvider;
54
- Tooltip: typeof Tooltip;
55
- TooltipTrigger: typeof TooltipTrigger;
56
- TooltipContent: typeof TooltipContent;
57
- }
58
- }
@@ -1,49 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import { useTrueFoundryToolResponses } from "@truefoundry/assistant-ui-runtime";
5
- import { useThreadIsRunning } from "@assistant-ui/core/react";
6
-
7
- import { useSlot } from "../theme/SlotsProvider.js";
8
- import type { AskUserOption } from "../atoms/AskUserPrompt.js";
9
-
10
- const OTHER_OPTION_ID = "__other";
11
-
12
- export function AskUserContainer() {
13
- const AskUserPrompt = useSlot("AskUserPrompt");
14
- const { pending, respond } = useTrueFoundryToolResponses();
15
- const isRunning = useThreadIsRunning();
16
- const [selectedOptionId, setSelectedOptionId] = useState<string | undefined>(undefined);
17
- const [otherValue, setOtherValue] = useState("");
18
-
19
- const item = pending[0];
20
- if (item == null) return null;
21
-
22
- const options: AskUserOption[] = (item.options ?? []).map((option) => ({ id: option, label: option }));
23
- const allowOther = options.length === 0;
24
-
25
- const onSubmit = () => {
26
- const content =
27
- selectedOptionId != null && selectedOptionId !== OTHER_OPTION_ID
28
- ? (options.find((o) => o.id === selectedOptionId)?.label ?? "")
29
- : otherValue.trim();
30
- if (!content) return;
31
- respond({ toolCallId: item.toolCallId, content });
32
- setSelectedOptionId(undefined);
33
- setOtherValue("");
34
- };
35
-
36
- return (
37
- <AskUserPrompt
38
- question={item.question ?? "Answer required"}
39
- options={options}
40
- allowOther={allowOther}
41
- selectedOptionId={selectedOptionId}
42
- otherValue={otherValue}
43
- disabled={isRunning}
44
- onSelectOption={setSelectedOptionId}
45
- onOtherValueChange={setOtherValue}
46
- onSubmit={onSubmit}
47
- />
48
- );
49
- }
@@ -1,56 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { render, screen } from "@testing-library/react";
3
- import { ThreadPrimitive, type ThreadMessageLike } from "@assistant-ui/react";
4
- import { describe, expect, it } from "vitest";
5
-
6
- import { RuntimeHarness } from "./RuntimeHarness.js";
7
- import { AssistantMessageContainer } from "./AssistantMessageContainer.js";
8
-
9
- function renderAssistantMessage(
10
- messages: ThreadMessageLike[],
11
- options?: { isRunning?: boolean },
12
- ) {
13
- return render(
14
- <RuntimeHarness messages={messages} isRunning={options?.isRunning}>
15
- <ThreadPrimitive.Messages>{() => <AssistantMessageContainer />}</ThreadPrimitive.Messages>
16
- </RuntimeHarness>,
17
- );
18
- }
19
-
20
- describe("AssistantMessageContainer", () => {
21
- it("renders assistant text through the Markdown atom", () => {
22
- renderAssistantMessage([{ role: "assistant", content: "hello world" }]);
23
- expect(screen.getByText("hello world")).toBeInTheDocument();
24
- });
25
-
26
- it("passes the message error to MessageErrorBanner when the message failed", () => {
27
- renderAssistantMessage([
28
- {
29
- role: "assistant",
30
- content: "partial",
31
- status: { type: "incomplete", reason: "error", error: "boom" },
32
- },
33
- ]);
34
- expect(screen.getByRole("alert")).toHaveTextContent("boom");
35
- });
36
-
37
- it("does not render an error banner for a healthy message", () => {
38
- renderAssistantMessage([{ role: "assistant", content: "all good" }]);
39
- expect(screen.queryByRole("alert")).not.toBeInTheDocument();
40
- });
41
-
42
- it("hides the action bar while the thread is running (streaming state)", () => {
43
- renderAssistantMessage([{ role: "assistant", content: "typing..." }], { isRunning: true });
44
- expect(screen.queryByRole("button", { name: "Copy" })).not.toBeInTheDocument();
45
- });
46
-
47
- it("shows the action bar once the thread is idle", () => {
48
- renderAssistantMessage([{ role: "assistant", content: "done" }], { isRunning: false });
49
- expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument();
50
- });
51
-
52
- it("hides the branch indicator when there is only a single branch", () => {
53
- renderAssistantMessage([{ role: "assistant", content: "hi" }]);
54
- expect(screen.queryByText(/^\d+ \/ \d+$/)).not.toBeInTheDocument();
55
- });
56
- });
@@ -1,128 +0,0 @@
1
- "use client";
2
-
3
- import { groupPartByType, MessagePrimitive, useAui, type EnrichedPartState } from "@assistant-ui/react";
4
- import {
5
- useActionBarCopy,
6
- useMessageBranching,
7
- useMessageError,
8
- useThreadIsRunning,
9
- } from "@assistant-ui/core/react";
10
-
11
- import { useSlot } from "../theme/SlotsProvider.js";
12
- import { AssistantTextContainer } from "./AssistantTextContainer.js";
13
- import { MessageImageContainer } from "./MessageImageContainer.js";
14
- import { ReasoningContainer } from "./ReasoningContainer.js";
15
- import { ToolCallContainer } from "./ToolCallContainer.js";
16
- import { ToolGroupContainer, type ThreadGroupPart } from "./ToolGroupContainer.js";
17
-
18
- /**
19
- * Dispatches a leaf message part to its renderer. "data" parts (generative
20
- * UI) are out of scope for this SDK entirely.
21
- */
22
- function AssistantLeafPartContainer({ part }: { part: EnrichedPartState }) {
23
- switch (part.type) {
24
- case "text":
25
- case "reasoning":
26
- return <AssistantTextContainer />;
27
- case "image":
28
- return <MessageImageContainer />;
29
- case "tool-call":
30
- return <ToolCallContainer {...part} />;
31
- default:
32
- return null;
33
- }
34
- }
35
-
36
- function downloadMarkdown(text: string) {
37
- if (typeof document === "undefined") return;
38
- const blob = new Blob([text], { type: "text/markdown" });
39
- const url = URL.createObjectURL(blob);
40
- const anchor = document.createElement("a");
41
- anchor.href = url;
42
- anchor.download = "message.md";
43
- anchor.click();
44
- URL.revokeObjectURL(url);
45
- }
46
-
47
- export function AssistantMessageContainer() {
48
- const MessageBubble = useSlot("MessageBubble");
49
- const MessageIndicator = useSlot("MessageIndicator");
50
- const MessageErrorBanner = useSlot("MessageErrorBanner");
51
- const MessageActionBar = useSlot("MessageActionBar");
52
- const BranchIndicator = useSlot("BranchIndicator");
53
-
54
- const aui = useAui();
55
- const isRunning = useThreadIsRunning();
56
- const error = useMessageError();
57
- const { copy, isCopied } = useActionBarCopy({
58
- copyToClipboard: (text) => navigator.clipboard.writeText(text),
59
- });
60
- const { branchNumber, branchCount, goToPrev, goToNext } = useMessageBranching();
61
-
62
- return (
63
- <MessagePrimitive.Root data-role="assistant">
64
- <MessageBubble
65
- variant="assistant"
66
- error={error !== undefined ? <MessageErrorBanner message={String(error)} /> : undefined}
67
- branchIndicator={
68
- <BranchIndicator
69
- index={branchNumber}
70
- count={branchCount}
71
- onPrevious={goToPrev}
72
- onNext={goToNext}
73
- />
74
- }
75
- actionBar={
76
- !isRunning ? (
77
- <MessageActionBar
78
- isCopied={isCopied}
79
- onCopy={copy}
80
- onExportMarkdown={() => downloadMarkdown(aui.message().getCopyText())}
81
- />
82
- ) : undefined
83
- }
84
- >
85
- <MessagePrimitive.GroupedParts
86
- groupBy={groupPartByType({
87
- reasoning: ["group-chainOfThought", "group-reasoning"],
88
- "tool-call": ["group-chainOfThought", "group-tool"],
89
- "standalone-tool-call": [],
90
- })}
91
- >
92
- {({ part, children }) => {
93
- switch (part.type) {
94
- case "group-chainOfThought":
95
- return (
96
- <div data-slot="aui_chain-of-thought" className="flex flex-col gap-3">
97
- {children}
98
- </div>
99
- );
100
- case "group-tool":
101
- return (
102
- <ToolGroupContainer group={part as ThreadGroupPart}>
103
- {children}
104
- </ToolGroupContainer>
105
- );
106
- case "group-reasoning":
107
- return (
108
- <ReasoningContainer group={part as ThreadGroupPart}>
109
- {children}
110
- </ReasoningContainer>
111
- );
112
- case "text":
113
- case "reasoning":
114
- case "tool-call":
115
- case "image":
116
- case "data":
117
- return <AssistantLeafPartContainer part={part} />;
118
- case "indicator":
119
- return <MessageIndicator />;
120
- default:
121
- return null;
122
- }
123
- }}
124
- </MessagePrimitive.GroupedParts>
125
- </MessageBubble>
126
- </MessagePrimitive.Root>
127
- );
128
- }
@@ -1,111 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { render, screen } from "@testing-library/react";
3
- import { ThreadPrimitive, type ThreadMessageLike } from "@assistant-ui/react";
4
- import { describe, expect, it } from "vitest";
5
-
6
- import { RuntimeHarness } from "./RuntimeHarness.js";
7
- import { AssistantMessageContainer } from "./AssistantMessageContainer.js";
8
-
9
- function renderAssistantMessage(messages: ThreadMessageLike[]) {
10
- return render(
11
- <RuntimeHarness messages={messages}>
12
- <ThreadPrimitive.Messages>{() => <AssistantMessageContainer />}</ThreadPrimitive.Messages>
13
- </RuntimeHarness>,
14
- );
15
- }
16
-
17
- describe("AssistantTextContainer", () => {
18
- it("renders markdown formatting from the live text part", () => {
19
- renderAssistantMessage([{ role: "assistant", content: "**bold** text" }]);
20
- const strong = screen.getByText("bold");
21
- expect(strong.tagName).toBe("STRONG");
22
- });
23
-
24
- it("renders plain streaming text as it grows", () => {
25
- renderAssistantMessage([{ role: "assistant", content: "partial toke" }]);
26
- expect(screen.getByText("partial toke")).toBeInTheDocument();
27
- });
28
-
29
- it("renders openui fenced blocks through OpenUIBlock instead of a code pre", () => {
30
- renderAssistantMessage([
31
- {
32
- role: "assistant",
33
- content: '```openui\nCard() { title: "Sales" }\n```',
34
- },
35
- ]);
36
- expect(screen.getByTestId("openui-renderer")).toHaveTextContent('Card() { title: "Sales" }');
37
- expect(document.querySelector(".aui-code-header-root")).not.toBeInTheDocument();
38
- });
39
-
40
- it("renders a fenced sandbox_artifacts code block as a download button", () => {
41
- renderAssistantMessage([
42
- {
43
- role: "assistant",
44
- content: "Here is a downloadable file:\n\n```sandbox_artifacts\n[sample.js](/tmp/sample.js)\n```",
45
- },
46
- ]);
47
- expect(screen.getByRole("button", { name: /sample\.js/ })).toBeInTheDocument();
48
- expect(document.querySelector(".aui-code-header-root")).not.toBeInTheDocument();
49
- });
50
-
51
- it("renders multiple links inside a fenced sandbox_artifacts code block", () => {
52
- renderAssistantMessage([
53
- {
54
- role: "assistant",
55
- content: "```sandbox_artifact\n[a.js](/tmp/a.js)\n[b.js](/tmp/b.js)\n```",
56
- },
57
- ]);
58
- expect(screen.getByRole("button", { name: /a\.js/ })).toBeInTheDocument();
59
- expect(screen.getByRole("button", { name: /b\.js/ })).toBeInTheDocument();
60
- });
61
-
62
- it("renders a single sandbox_artifact link as a download button", () => {
63
- renderAssistantMessage([
64
- {
65
- role: "assistant",
66
- content: "sandbox_artifact [Download the pink dog SVG](/tmp/pink_dog.svg)",
67
- },
68
- ]);
69
- const button = screen.getByRole("button", { name: /Download the pink dog SVG/ });
70
- expect(button).toBeInTheDocument();
71
- expect(screen.queryByRole("link")).not.toBeInTheDocument();
72
- });
73
-
74
- it("renders multiple sandbox_artifact links in one paragraph as separate buttons", () => {
75
- renderAssistantMessage([
76
- {
77
- role: "assistant",
78
- content:
79
- "sandbox_artifact [Download A](/tmp/a.svg) [Download B](/tmp/b.svg)",
80
- },
81
- ]);
82
- expect(screen.getByRole("button", { name: /Download A/ })).toBeInTheDocument();
83
- expect(screen.getByRole("button", { name: /Download B/ })).toBeInTheDocument();
84
- });
85
-
86
- it("renders two separate sandbox_artifact paragraphs as two independent lists", () => {
87
- renderAssistantMessage([
88
- {
89
- role: "assistant",
90
- content:
91
- "sandbox_artifact [Download A](/tmp/a.svg)\n\nsandbox_artifact [Download B](/tmp/b.svg)",
92
- },
93
- ]);
94
- expect(screen.getByRole("button", { name: /Download A/ })).toBeInTheDocument();
95
- expect(screen.getByRole("button", { name: /Download B/ })).toBeInTheDocument();
96
- });
97
-
98
- it("renders a plain paragraph without the token unaffected", () => {
99
- renderAssistantMessage([{ role: "assistant", content: "just a [link](/tmp/a.svg)" }]);
100
- expect(screen.getByRole("link", { name: "link" })).toBeInTheDocument();
101
- expect(screen.queryByRole("button", { name: /link/ })).not.toBeInTheDocument();
102
- });
103
-
104
- it("falls back to default paragraph rendering for a partial, unclosed artifact link mid-stream", () => {
105
- renderAssistantMessage([
106
- { role: "assistant", content: "sandbox_artifact [Download the pink dog SVG](/tmp/pin" },
107
- ]);
108
- expect(screen.queryByRole("button", { name: /Download/ })).not.toBeInTheDocument();
109
- expect(screen.getByText(/sandbox_artifact/)).toBeInTheDocument();
110
- });
111
- });
@@ -1,54 +0,0 @@
1
- "use client";
2
-
3
- import { useCallback } from "react";
4
- import { useAuiState } from "@assistant-ui/react";
5
- import { useTrueFoundryDownloadSandboxFile } from "@truefoundry/assistant-ui-runtime";
6
-
7
- import { useSlot } from "../theme/SlotsProvider.js";
8
- import { useErrorToasterOptional } from "./ErrorToasterContainer.js";
9
-
10
- function filenameFromPath(path: string): string {
11
- return path.split("/").pop() || "download";
12
- }
13
-
14
- function triggerBrowserDownload(blob: Blob, filename: string) {
15
- if (typeof document === "undefined") return;
16
- const url = URL.createObjectURL(blob);
17
- const anchor = document.createElement("a");
18
- anchor.href = url;
19
- anchor.download = filename;
20
- anchor.click();
21
- URL.revokeObjectURL(url);
22
- }
23
-
24
- export function AssistantTextContainer() {
25
- const Markdown = useSlot("Markdown");
26
- const downloadSandboxFile = useTrueFoundryDownloadSandboxFile();
27
- const errorToaster = useErrorToasterOptional();
28
- const text = useAuiState((s) =>
29
- s.part.type === "text" || s.part.type === "reasoning" ? s.part.text : "",
30
- );
31
- const isStreaming = useAuiState((s) => {
32
- if (s.message.status?.type !== "running") return false;
33
- const lastIndex = s.message.parts.length - 1;
34
- if (lastIndex < 0) return false;
35
- if (s.part.type !== "text" && s.part.type !== "reasoning") return false;
36
- return s.message.parts[lastIndex] === s.part;
37
- });
38
-
39
- const handleDownloadArtifact = useCallback(
40
- (path: string) =>
41
- downloadSandboxFile(path)
42
- .then((blob) => triggerBrowserDownload(blob, filenameFromPath(path)))
43
- .catch((error) => {
44
- if (errorToaster != null) {
45
- errorToaster.showError(error);
46
- } else {
47
- console.error("Failed to download sandbox artifact", error);
48
- }
49
- }),
50
- [downloadSandboxFile, errorToaster],
51
- );
52
-
53
- return <Markdown content={text} isStreaming={isStreaming} onDownloadArtifact={handleDownloadArtifact} />;
54
- }
@@ -1,83 +0,0 @@
1
- "use client";
2
-
3
- import { ComposerPrimitive, MessagePrimitive, useAui, useAuiState } from "@assistant-ui/react";
4
-
5
- import { USER_MESSAGE_ATTACHMENT_PREVIEW_REM } from "../atoms/AttachmentCard.js";
6
- import { useSlot } from "../theme/SlotsProvider.js";
7
- import { isImageAttachment, useAttachmentPreviewSrc } from "./useAttachmentPreviewSrc.js";
8
-
9
- function ComposerAttachmentItem() {
10
- const AttachmentCard = useSlot("AttachmentCard");
11
- const AttachmentPreviewDialog = useSlot("AttachmentPreviewDialog");
12
- const aui = useAui();
13
- const name = useAuiState((s) => s.attachment.name);
14
- const contentType = useAuiState((s) => s.attachment.contentType);
15
- const type = useAuiState((s) => s.attachment.type);
16
- const isImage = isImageAttachment(type, contentType);
17
- const previewSrc = useAttachmentPreviewSrc();
18
-
19
- return (
20
- <AttachmentPreviewDialog previewSrc={previewSrc}>
21
- <AttachmentCard
22
- name={name}
23
- contentType={contentType}
24
- previewSrc={previewSrc}
25
- isImage={isImage}
26
- size="chip"
27
- onRemove={() => void aui.attachment().remove()}
28
- />
29
- </AttachmentPreviewDialog>
30
- );
31
- }
32
-
33
- function MessageAttachmentItem() {
34
- const AttachmentCard = useSlot("AttachmentCard");
35
- const AttachmentPreviewDialog = useSlot("AttachmentPreviewDialog");
36
- const name = useAuiState((s) => s.attachment.name);
37
- const contentType = useAuiState((s) => s.attachment.contentType);
38
- const type = useAuiState((s) => s.attachment.type);
39
- const isImage = isImageAttachment(type, contentType);
40
- const previewSrc = useAttachmentPreviewSrc();
41
-
42
- const card = (
43
- <AttachmentCard
44
- name={name}
45
- contentType={contentType}
46
- previewSrc={previewSrc}
47
- isImage={isImage}
48
- size={isImage ? "preview" : "chip"}
49
- previewRem={USER_MESSAGE_ATTACHMENT_PREVIEW_REM}
50
- />
51
- );
52
-
53
- if (isImage && previewSrc) {
54
- return <AttachmentPreviewDialog previewSrc={previewSrc}>{card}</AttachmentPreviewDialog>;
55
- }
56
-
57
- return card;
58
- }
59
-
60
- export function ComposerAttachmentsContainer() {
61
- return (
62
- <div className="aui-composer-attachments flex w-full flex-row flex-wrap items-center gap-2 empty:hidden">
63
- <ComposerPrimitive.Attachments>{() => <ComposerAttachmentItem />}</ComposerPrimitive.Attachments>
64
- </div>
65
- );
66
- }
67
-
68
- export function MessageAttachmentsContainer() {
69
- return (
70
- <div className="aui-user-message-attachments-end col-span-full col-start-1 row-start-1 flex w-full flex-row justify-end gap-2">
71
- <MessagePrimitive.Attachments>{() => <MessageAttachmentItem />}</MessagePrimitive.Attachments>
72
- </div>
73
- );
74
- }
75
-
76
- export function ComposerAttachmentPickerContainer() {
77
- const AttachmentPickerButton = useSlot("AttachmentPickerButton");
78
- return (
79
- <ComposerPrimitive.AddAttachment asChild>
80
- <AttachmentPickerButton />
81
- </ComposerPrimitive.AddAttachment>
82
- );
83
- }
@@ -1,66 +0,0 @@
1
- "use client";
2
-
3
- import { useRef } from "react";
4
- import { useAui, useAuiState } from "@assistant-ui/react";
5
- import { useTrueFoundryCancel, useTrueFoundryToolResponses } from "@truefoundry/assistant-ui-runtime";
6
-
7
- import { useComposerBusyState } from "../hooks/useComposerBusyState.js";
8
- import { useSlot } from "../theme/SlotsProvider.js";
9
- import { ComposerAttachmentsContainer } from "./AttachmentsContainer.js";
10
- import { AskUserContainer } from "./AskUserContainer.js";
11
- import { McpAuthContainer } from "./McpAuthContainer.js";
12
-
13
- const threadHasPendingMcpAuth = (s: { thread: { messages: readonly { role: string; status?: { type: string }; metadata?: { custom?: unknown } }[] } }) => {
14
- const messages = s.thread.messages;
15
- const last = messages[messages.length - 1];
16
- if (last?.role !== "assistant") return false;
17
- if (last.status?.type !== "requires-action") return false;
18
- return (last.metadata?.custom as { pendingMcpAuth?: boolean } | undefined)?.pendingMcpAuth === true;
19
- };
20
-
21
- export function ComposerContainer() {
22
- const ComposerShell = useSlot("ComposerShell");
23
- const aui = useAui();
24
- const text = useAuiState((s) => s.composer.text);
25
- const { isBusy, send, resetBusy } = useComposerBusyState();
26
- const mcpPending = useAuiState(threadHasPendingMcpAuth);
27
- const { pending: toolResponsesPending } = useTrueFoundryToolResponses();
28
- const cancel = useTrueFoundryCancel();
29
- const fileInputRef = useRef<HTMLInputElement>(null);
30
-
31
- if (mcpPending) {
32
- return <McpAuthContainer />;
33
- }
34
- if (toolResponsesPending.length > 0) {
35
- return <AskUserContainer />;
36
- }
37
-
38
- return (
39
- <>
40
- <input
41
- ref={fileInputRef}
42
- type="file"
43
- hidden
44
- onChange={(event) => {
45
- const file = event.target.files?.[0];
46
- if (file) void aui.composer().addAttachment(file);
47
- event.target.value = "";
48
- }}
49
- />
50
- <ComposerShell
51
- attachments={<ComposerAttachmentsContainer />}
52
- value={text}
53
- placeholder="Ask anything... (Shift+Enter for new line)"
54
- disabled={isBusy}
55
- isRunning={isBusy}
56
- onValueChange={(value) => aui.composer().setText(value)}
57
- onSubmit={() => send(() => aui.composer().send())}
58
- onCancel={() => {
59
- resetBusy();
60
- void cancel();
61
- }}
62
- onAttach={() => fileInputRef.current?.click()}
63
- />
64
- </>
65
- );
66
- }