@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,167 +0,0 @@
1
- "use client";
2
-
3
- import { useState } from "react";
4
- import {
5
- MessagePrimitive,
6
- useToolCallElapsed,
7
- type ToolApprovalOption as AuiToolApprovalOption,
8
- type ToolApprovalResponse,
9
- type ToolCallMessagePartComponent,
10
- type ToolCallMessagePartProps,
11
- } from "@assistant-ui/react";
12
- import { useTrueFoundryRespondToToolApproval } from "@truefoundry/assistant-ui-runtime";
13
-
14
- import { useSlot } from "../theme/SlotsProvider.js";
15
- import type { ToolApprovalOption } from "../atoms/ToolApprovalBar.js";
16
- import type { ToolCallStatus } from "../atoms/ToolCallCard.js";
17
- import { NestedApprovalBridgeContext, useNestedApprovalBridge } from "./nestedApprovalBridge.js";
18
-
19
- const SUB_AGENT_TOOL_NAME = "create_sub_agent";
20
-
21
- const APPROVAL_OPTION_DEFAULT_LABELS: Record<string, string> = {
22
- "allow-once": "Allow",
23
- "allow-always": "Always allow",
24
- "reject-once": "Deny",
25
- "reject-always": "Always deny",
26
- };
27
-
28
- const isAllowKind = (kind: string) => kind === "allow-once" || kind === "allow-always";
29
-
30
- function hasPendingToolApproval(approval: { approved?: boolean; resolution?: "cancelled" | "expired" } | undefined) {
31
- return approval != null && approval.approved === undefined && approval.resolution === undefined;
32
- }
33
-
34
- function buildApprovalOptions(options: readonly AuiToolApprovalOption[] | undefined): ToolApprovalOption[] {
35
- const declared = options?.filter((o) => Object.hasOwn(APPROVAL_OPTION_DEFAULT_LABELS, o.kind));
36
- if (declared && declared.length > 0) {
37
- const allow = declared.filter((o) => isAllowKind(o.kind));
38
- const reject = declared.filter((o) => !isAllowKind(o.kind));
39
- const mapped: ToolApprovalOption[] = [...allow, ...reject].map((o) => ({
40
- id: o.id,
41
- label: o.label ?? APPROVAL_OPTION_DEFAULT_LABELS[o.kind] ?? o.id,
42
- isAllow: isAllowKind(o.kind),
43
- grants: o.grants,
44
- confirm:
45
- o.confirm != null ? (typeof o.confirm === "object" ? o.confirm : {}) : undefined,
46
- }));
47
- if (reject.length === 0) {
48
- mapped.push({ id: "__deny", label: "Deny", isAllow: false });
49
- }
50
- return mapped;
51
- }
52
- return [
53
- { id: "__allow", label: "Allow", isAllow: true },
54
- { id: "__deny", label: "Deny", isAllow: false },
55
- ];
56
- }
57
-
58
- function formatDuration(ms: number): string {
59
- if (ms < 1000) return "<1s";
60
- const seconds = ms / 1000;
61
- if (seconds < 10) return `${(Math.floor(seconds * 10) / 10).toFixed(1)}s`;
62
- if (seconds < 60) return `${Math.floor(seconds)}s`;
63
- return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`;
64
- }
65
-
66
- function toStatus(statusType: string | undefined): ToolCallStatus {
67
- if (statusType === "complete") return "success";
68
- if (statusType === "incomplete") return "error";
69
- return "running";
70
- }
71
-
72
- function ToolApprovalSlot({ part }: { part: ToolCallMessagePartProps }) {
73
- const ToolApprovalBar = useSlot("ToolApprovalBar");
74
- const nestedBridge = useNestedApprovalBridge();
75
-
76
- const respond = (response: ToolApprovalResponse) => {
77
- if (nestedBridge) {
78
- nestedBridge(response);
79
- return;
80
- }
81
- part.respondToApproval(response);
82
- };
83
-
84
- const onSelectOption = (optionId: string) => {
85
- if (optionId === "__allow") return respond({ approved: true });
86
- if (optionId === "__deny") return respond({ approved: false });
87
- return respond({ optionId });
88
- };
89
-
90
- return <ToolApprovalBar options={buildApprovalOptions(part.approval?.options)} onSelectOption={onSelectOption} />;
91
- }
92
-
93
- export const ToolCallContainer: ToolCallMessagePartComponent = (part) => {
94
- const ToolCallCard = useSlot("ToolCallCard");
95
- const respondToNestedApproval = useTrueFoundryRespondToToolApproval();
96
- const elapsedMs = useToolCallElapsed();
97
- const isRequiresAction = part.status?.type === "requires-action";
98
- const [expanded, setExpanded] = useState(isRequiresAction);
99
- const [prevRequiresAction, setPrevRequiresAction] = useState(isRequiresAction);
100
- if (isRequiresAction !== prevRequiresAction) {
101
- setPrevRequiresAction(isRequiresAction);
102
- if (isRequiresAction) setExpanded(true);
103
- }
104
-
105
- const showApproval = isRequiresAction && hasPendingToolApproval(part.approval);
106
- const isSubAgent = part.toolName === SUB_AGENT_TOOL_NAME;
107
-
108
- const durationText = elapsedMs === undefined ? undefined : formatDuration(elapsedMs);
109
- const status = toStatus(part.status?.type);
110
-
111
- if (isSubAgent) {
112
- const firstNested = part.messages?.[0];
113
- const subAgent = (firstNested?.metadata?.custom as { subAgent?: { title?: string; name?: string; input?: string } } | undefined)
114
- ?.subAgent;
115
- const agentName = subAgent?.title ?? subAgent?.name ?? part.toolName;
116
- const instruction = subAgent?.input ?? "";
117
- const stepCount = part.messages?.length ?? 0;
118
-
119
- const bridge = (response: ToolApprovalResponse) => {
120
- if (part.approval == null) return;
121
- const approved = "approved" in response ? response.approved : undefined;
122
- if (approved === undefined) return;
123
- respondToNestedApproval({ approvalId: part.approval.id, approved });
124
- };
125
-
126
- return (
127
- <ToolCallCard
128
- variant="sub-agent"
129
- name={part.toolName}
130
- status={status}
131
- expanded={expanded}
132
- onToggle={() => setExpanded((prev) => !prev)}
133
- durationText={durationText}
134
- agentName={agentName}
135
- instruction={instruction}
136
- stepCount={stepCount}
137
- >
138
- <NestedApprovalBridgeContext.Provider value={bridge}>
139
- <MessagePrimitive.Root data-role="assistant">
140
- <MessagePrimitive.Parts components={{ tools: { Fallback: ToolCallContainer } }} />
141
- </MessagePrimitive.Root>
142
- </NestedApprovalBridgeContext.Provider>
143
- </ToolCallCard>
144
- );
145
- }
146
-
147
- return (
148
- <ToolCallCard
149
- variant="tool"
150
- name={part.toolName}
151
- status={status}
152
- expanded={expanded}
153
- onToggle={() => setExpanded((prev) => !prev)}
154
- durationText={durationText}
155
- argsText={part.argsText}
156
- result={
157
- part.result === undefined
158
- ? undefined
159
- : typeof part.result === "string"
160
- ? part.result
161
- : JSON.stringify(part.result, null, 2)
162
- }
163
- isError={part.isError}
164
- approvalSlot={showApproval ? <ToolApprovalSlot part={part} /> : undefined}
165
- />
166
- );
167
- };
@@ -1,56 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { render, screen, fireEvent } 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 renderMultiToolMessage(content: ThreadMessageLike["content"], isRunning = false) {
10
- const message: ThreadMessageLike = { role: "assistant", content };
11
- return render(
12
- <RuntimeHarness messages={[message]} isRunning={isRunning}>
13
- <ThreadPrimitive.Messages>{() => <AssistantMessageContainer />}</ThreadPrimitive.Messages>
14
- </RuntimeHarness>,
15
- );
16
- }
17
-
18
- describe("ToolGroupContainer", () => {
19
- it("groups consecutive tool calls under a single 'N tool calls' trigger", () => {
20
- renderMultiToolMessage([
21
- { type: "tool-call", toolCallId: "1", toolName: "first_tool", args: {}, result: "a" },
22
- { type: "tool-call", toolCallId: "2", toolName: "second_tool", args: {}, result: "b" },
23
- ]);
24
- expect(screen.getByText("2 tool calls")).toBeInTheDocument();
25
- });
26
-
27
- it("renders both nested tool calls once expanded (defaultOpen)", () => {
28
- renderMultiToolMessage([
29
- { type: "tool-call", toolCallId: "1", toolName: "first_tool", args: {} },
30
- { type: "tool-call", toolCallId: "2", toolName: "second_tool", args: {} },
31
- ]);
32
- expect(screen.getByText("first_tool")).toBeInTheDocument();
33
- expect(screen.getByText("second_tool")).toBeInTheDocument();
34
- });
35
-
36
- it("collapses the group when the trigger is toggled", () => {
37
- renderMultiToolMessage([
38
- { type: "tool-call", toolCallId: "1", toolName: "first_tool", args: {} },
39
- { type: "tool-call", toolCallId: "2", toolName: "second_tool", args: {} },
40
- ]);
41
- fireEvent.click(screen.getByText("2 tool calls"));
42
- expect(screen.queryByText("first_tool")).not.toBeInTheDocument();
43
- });
44
-
45
- it("uses singular label for exactly one tool call in the group", () => {
46
- // A single standalone tool-call does not form a group; use two calls
47
- // where only one belongs to the grouped chain-of-thought bucket is not
48
- // constructible via plain fixtures, so this covers the pluralization
49
- // branch directly through the atom in ToolGroupCard's own contract.
50
- renderMultiToolMessage([
51
- { type: "tool-call", toolCallId: "1", toolName: "first_tool", args: {} },
52
- { type: "tool-call", toolCallId: "2", toolName: "second_tool", args: {} },
53
- ]);
54
- expect(screen.queryByText("1 tool call")).not.toBeInTheDocument();
55
- });
56
- });
@@ -1,24 +0,0 @@
1
- "use client";
2
-
3
- import { useState, type PropsWithChildren } from "react";
4
- import type { MessagePrimitive } from "@assistant-ui/react";
5
-
6
- import { useSlot } from "../theme/SlotsProvider.js";
7
-
8
- export type ThreadGroupPart = MessagePrimitive.GroupedParts.GroupPart;
9
-
10
- export function ToolGroupContainer({ group, children }: PropsWithChildren<{ group: ThreadGroupPart }>) {
11
- const ToolGroupCard = useSlot("ToolGroupCard");
12
- const [expanded, setExpanded] = useState(true);
13
-
14
- return (
15
- <ToolGroupCard
16
- toolCallCount={group.indices.length}
17
- active={group.status.type === "running"}
18
- expanded={expanded}
19
- onToggle={() => setExpanded((prev) => !prev)}
20
- >
21
- {children}
22
- </ToolGroupCard>
23
- );
24
- }
@@ -1,52 +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 { UserEditComposerContainer } from "./UserEditComposerContainer.js";
8
-
9
- function renderEditComposer(messages: ThreadMessageLike[]) {
10
- return render(
11
- <RuntimeHarness messages={messages}>
12
- <ThreadPrimitive.Messages>{() => <UserEditComposerContainer />}</ThreadPrimitive.Messages>
13
- </RuntimeHarness>,
14
- );
15
- }
16
-
17
- describe("UserEditComposerContainer", () => {
18
- it("renders a text input with cancel and send actions", () => {
19
- renderEditComposer([{ role: "user", content: "edit me", id: "turn-1-user" }]);
20
- expect(screen.getByRole("textbox")).toBeInTheDocument();
21
- expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument();
22
- expect(screen.getByRole("button", { name: "Send" })).toBeInTheDocument();
23
- });
24
-
25
- it("shows read-only attachments without remove controls", () => {
26
- renderEditComposer([
27
- {
28
- role: "user",
29
- content: "What is this?",
30
- id: "turn-1-user",
31
- attachments: [
32
- {
33
- id: "att-1",
34
- type: "image",
35
- name: "logo.png",
36
- contentType: "image/png",
37
- status: { type: "complete" },
38
- content: [
39
- {
40
- type: "image",
41
- image: "data:image/png;base64,iVBORw0KGgo=",
42
- filename: "logo.png",
43
- },
44
- ],
45
- },
46
- ],
47
- },
48
- ]);
49
- expect(screen.getByAltText("logo.png")).toBeInTheDocument();
50
- expect(screen.queryByRole("button", { name: /remove/i })).not.toBeInTheDocument();
51
- });
52
- });
@@ -1,86 +0,0 @@
1
- "use client";
2
-
3
- import {
4
- ComposerPrimitive,
5
- MessagePrimitive,
6
- useAuiState,
7
- } from "@assistant-ui/react";
8
- import { useThreadIsRunning } from "@assistant-ui/core/react";
9
-
10
- import { useSlot } from "../theme/SlotsProvider.js";
11
- import { MessageAttachmentsContainer } from "./AttachmentsContainer.js";
12
- import { cn } from "../atoms/lib/cn.js";
13
-
14
- function ReadOnlyMessageAttachments() {
15
- const hasAttachments = useAuiState(
16
- (s) => (s.message.attachments?.length ?? 0) > 0,
17
- );
18
- if (!hasAttachments) {
19
- return null;
20
- }
21
- return (
22
- <div className="pointer-events-none opacity-90">
23
- <MessageAttachmentsContainer />
24
- </div>
25
- );
26
- }
27
-
28
- export function UserEditComposerContainer() {
29
- const Button = useSlot("Button");
30
- const isRunning = useThreadIsRunning();
31
-
32
- return (
33
- <MessagePrimitive.Root data-role="user">
34
- <div
35
- data-slot="aui_user-edit-composer-root"
36
- className="fade-in slide-in-from-bottom-1 animate-in grid auto-rows-auto grid-cols-[minmax(72px,1fr)_auto] content-start gap-y-2 px-2 duration-150 [&:where(>*)]:col-start-2"
37
- >
38
- <ReadOnlyMessageAttachments />
39
- <ComposerPrimitive.Root asChild>
40
- <div className="aui-user-edit-composer col-start-2 min-w-0">
41
- <div className="ring-primary/60 bg-muted rounded-xl px-4 py-3 ring-2">
42
- <ComposerPrimitive.Input
43
- disabled={isRunning}
44
- className="placeholder:text-muted-foreground/80 max-h-32 min-h-10 w-full resize-none bg-transparent text-base outline-none"
45
- />
46
- <div className="mt-3 flex items-end justify-between gap-3">
47
- <p className="text-muted-foreground text-xs">
48
- <kbd className="bg-background/60 rounded border px-1 py-0.5 font-sans text-[10px]">
49
- Enter
50
- </kbd>{" "}
51
- to save{" "}
52
- <kbd className="bg-background/60 rounded border px-1 py-0.5 font-sans text-[10px]">
53
- Esc
54
- </kbd>{" "}
55
- to cancel
56
- </p>
57
- <div className="flex items-center gap-2">
58
- <ComposerPrimitive.Cancel asChild>
59
- <Button
60
- type="button"
61
- variant="ghost"
62
- size="sm"
63
- className="text-muted-foreground h-8 px-2"
64
- >
65
- Cancel
66
- </Button>
67
- </ComposerPrimitive.Cancel>
68
- <ComposerPrimitive.Send asChild>
69
- <Button
70
- type="button"
71
- size="sm"
72
- className={cn("h-8 rounded-lg px-3")}
73
- disabled={isRunning}
74
- >
75
- Send
76
- </Button>
77
- </ComposerPrimitive.Send>
78
- </div>
79
- </div>
80
- </div>
81
- </div>
82
- </ComposerPrimitive.Root>
83
- </div>
84
- </MessagePrimitive.Root>
85
- );
86
- }
@@ -1,99 +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 { UserMessageContainer } from "./UserMessageContainer.js";
8
-
9
- function renderUserMessage(
10
- messages: ThreadMessageLike[],
11
- options?: { isRunning?: boolean },
12
- ) {
13
- return render(
14
- <RuntimeHarness messages={messages} isRunning={options?.isRunning}>
15
- <ThreadPrimitive.Messages>{() => <UserMessageContainer />}</ThreadPrimitive.Messages>
16
- </RuntimeHarness>,
17
- );
18
- }
19
-
20
- describe("UserMessageContainer", () => {
21
- it("renders user text content through the Markdown atom", () => {
22
- renderUserMessage([{ role: "user", content: "what is the capital of France?" }]);
23
- expect(screen.getByText("what is the capital of France?")).toBeInTheDocument();
24
- });
25
-
26
- it("renders an image attachment preview above the text bubble", () => {
27
- renderUserMessage([
28
- {
29
- role: "user",
30
- content: "What is this?",
31
- attachments: [
32
- {
33
- id: "att-1",
34
- type: "image",
35
- name: "logo.png",
36
- contentType: "image/png",
37
- status: { type: "complete" },
38
- content: [
39
- {
40
- type: "image",
41
- image: "data:image/png;base64,iVBORw0KGgo=",
42
- filename: "logo.png",
43
- },
44
- ],
45
- },
46
- ],
47
- },
48
- ]);
49
- expect(screen.getByText("What is this?")).toBeInTheDocument();
50
- const preview = screen.getByAltText("logo.png");
51
- expect(preview).toBeInTheDocument();
52
- expect(preview.parentElement).toHaveStyle({ width: "12rem", height: "12rem" });
53
- });
54
-
55
- it("renders a file attachment chip above the text bubble", () => {
56
- renderUserMessage([
57
- {
58
- role: "user",
59
- content: "See attached",
60
- attachments: [
61
- {
62
- id: "att-1",
63
- type: "file",
64
- name: "report.pdf",
65
- contentType: "application/pdf",
66
- status: { type: "complete" },
67
- content: [
68
- {
69
- type: "file",
70
- mimeType: "application/pdf",
71
- filename: "report.pdf",
72
- data: "data:application/pdf;base64,AAAA",
73
- },
74
- ],
75
- },
76
- ],
77
- },
78
- ]);
79
- expect(screen.getByText("See attached")).toBeInTheDocument();
80
- const chip = screen.getByText("report.pdf").closest("[data-slot='aui_attachment-chip']");
81
- expect(chip).toHaveStyle({ maxWidth: "12rem" });
82
- });
83
-
84
- it("shows the action bar when the thread is idle", () => {
85
- renderUserMessage([{ role: "user", content: "hello", id: "turn-1-user" }], {
86
- isRunning: false,
87
- });
88
- expect(screen.getByRole("button", { name: "Edit" })).toBeInTheDocument();
89
- expect(screen.getByRole("button", { name: "Reset" })).toBeInTheDocument();
90
- expect(screen.getByRole("button", { name: "Copy" })).toBeInTheDocument();
91
- });
92
-
93
- it("hides the action bar while the thread is running", () => {
94
- renderUserMessage([{ role: "user", content: "hello", id: "turn-1-user" }], {
95
- isRunning: true,
96
- });
97
- expect(screen.queryByRole("button", { name: "Edit" })).not.toBeInTheDocument();
98
- });
99
- });
@@ -1,26 +0,0 @@
1
- "use client";
2
-
3
- import { MessagePrimitive } from "@assistant-ui/react";
4
- import { useThreadIsRunning } from "@assistant-ui/core/react";
5
-
6
- import { useSlot } from "../theme/SlotsProvider.js";
7
- import { MessageAttachmentsContainer } from "./AttachmentsContainer.js";
8
- import { AssistantTextContainer } from "./AssistantTextContainer.js";
9
-
10
- export function UserMessageContainer() {
11
- const MessageBubble = useSlot("MessageBubble");
12
- const UserMessageActionBar = useSlot("UserMessageActionBar");
13
- const isRunning = useThreadIsRunning();
14
-
15
- return (
16
- <MessagePrimitive.Root data-role="user">
17
- <MessageBubble
18
- variant="user"
19
- attachments={<MessageAttachmentsContainer />}
20
- actionBar={!isRunning ? <UserMessageActionBar /> : undefined}
21
- >
22
- <MessagePrimitive.Parts components={{ Text: AssistantTextContainer }} />
23
- </MessageBubble>
24
- </MessagePrimitive.Root>
25
- );
26
- }
@@ -1,20 +0,0 @@
1
- "use client";
2
-
3
- import { createContext, useContext } from "react";
4
- import type { ToolApprovalResponse } from "@assistant-ui/react";
5
-
6
- /**
7
- * Sub-agent tool calls render inside a read-only nested thread context
8
- * (`MessagePartPrimitive.Messages`), so the standard `respondToApproval` prop
9
- * on their parts doesn't route back to the gateway. `ToolCallContainer` uses
10
- * this bridge instead when rendering nested calls, translating the response
11
- * through `useTrueFoundryRespondToToolApproval()` -- mirrors the reference's
12
- * `nestedToolCallMessageComponents` override.
13
- */
14
- export const NestedApprovalBridgeContext = createContext<((response: ToolApprovalResponse) => void) | null>(
15
- null,
16
- );
17
-
18
- export function useNestedApprovalBridge(): ((response: ToolApprovalResponse) => void) | null {
19
- return useContext(NestedApprovalBridgeContext);
20
- }
@@ -1,42 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useState } from "react";
4
- import { useAuiState } from "@assistant-ui/react";
5
-
6
- export function isImageAttachment(type: string, contentType?: string): boolean {
7
- return type === "image" || (contentType?.startsWith("image/") ?? false);
8
- }
9
-
10
- function useFileObjectUrl(file: File | undefined): string | undefined {
11
- const [src, setSrc] = useState<string | undefined>(undefined);
12
-
13
- useEffect(() => {
14
- if (!file) {
15
- setSrc(undefined);
16
- return;
17
- }
18
- const objectUrl = URL.createObjectURL(file);
19
- setSrc(objectUrl);
20
- return () => URL.revokeObjectURL(objectUrl);
21
- }, [file]);
22
-
23
- return src;
24
- }
25
-
26
- /** Preview URL for staged composer files (object URL) or sent message attachments (data URI). */
27
- export function useAttachmentPreviewSrc(): string | undefined {
28
- const type = useAuiState((s) => s.attachment.type);
29
- const contentType = useAuiState((s) => s.attachment.contentType);
30
- const file = useAuiState((s) => ("file" in s.attachment ? s.attachment.file : undefined));
31
- const contentSrc = useAuiState((s) => {
32
- if (!isImageAttachment(s.attachment.type, s.attachment.contentType)) return undefined;
33
- const imagePart = s.attachment.content?.find((part) => part.type === "image");
34
- if (imagePart?.type === "image" && imagePart.image) return imagePart.image;
35
- return undefined;
36
- });
37
-
38
- const image = isImageAttachment(type, contentType);
39
- const fileSrc = useFileObjectUrl(image ? file : undefined);
40
- if (!image) return undefined;
41
- return fileSrc ?? contentSrc;
42
- }
@@ -1,78 +0,0 @@
1
- // @vitest-environment jsdom
2
- import { createElement, type ReactNode } from "react";
3
- import { renderHook, act, waitFor } from "@testing-library/react";
4
- import { describe, expect, it, vi, beforeEach } from "vitest";
5
-
6
- const useThreadIsRunning = vi.fn(() => false);
7
-
8
- vi.mock("@assistant-ui/core/react", () => ({
9
- useThreadIsRunning: () => useThreadIsRunning(),
10
- }));
11
-
12
- import { ComposerBusyProvider, useComposerBusyState } from "./useComposerBusyState.js";
13
-
14
- function wrapper({ children }: { children: ReactNode }) {
15
- return createElement(ComposerBusyProvider, null, children);
16
- }
17
-
18
- describe("useComposerBusyState", () => {
19
- beforeEach(() => {
20
- useThreadIsRunning.mockReturnValue(false);
21
- });
22
-
23
- it("starts busy immediately on send before the thread is running", () => {
24
- const { result } = renderHook(() => useComposerBusyState(), { wrapper });
25
-
26
- act(() => {
27
- result.current.send(() => undefined);
28
- });
29
-
30
- expect(result.current.isBusy).toBe(true);
31
- expect(result.current.isSubmitting).toBe(true);
32
- expect(result.current.isRunning).toBe(false);
33
- });
34
-
35
- it("clears submitting when the thread stops running", async () => {
36
- useThreadIsRunning.mockReturnValue(true);
37
- const { result, rerender } = renderHook(() => useComposerBusyState(), { wrapper });
38
-
39
- act(() => {
40
- result.current.send(() => undefined);
41
- });
42
-
43
- useThreadIsRunning.mockReturnValue(false);
44
- rerender();
45
-
46
- await waitFor(() => {
47
- expect(result.current.isBusy).toBe(false);
48
- });
49
- });
50
-
51
- it("clears submitting when send rejects", async () => {
52
- const { result } = renderHook(() => useComposerBusyState(), { wrapper });
53
-
54
- act(() => {
55
- result.current.send(() => Promise.reject(new Error("send failed")));
56
- });
57
-
58
- expect(result.current.isBusy).toBe(true);
59
-
60
- await waitFor(() => {
61
- expect(result.current.isBusy).toBe(false);
62
- });
63
- });
64
-
65
- it("resetBusy clears optimistic submitting state", () => {
66
- const { result } = renderHook(() => useComposerBusyState(), { wrapper });
67
-
68
- act(() => {
69
- result.current.send(() => undefined);
70
- });
71
- expect(result.current.isBusy).toBe(true);
72
-
73
- act(() => {
74
- result.current.resetBusy();
75
- });
76
- expect(result.current.isBusy).toBe(false);
77
- });
78
- });