work-agent 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/README.md +234 -0
  2. package/app/(admin)/approvals/page.tsx +16 -0
  3. package/app/(admin)/audit/page.tsx +18 -0
  4. package/app/(admin)/layout.tsx +47 -0
  5. package/app/(admin)/scheduled-tasks/page.tsx +17 -0
  6. package/app/(admin)/settings/page.tsx +46 -0
  7. package/app/(admin)/skills/[name]/page.tsx +378 -0
  8. package/app/(admin)/skills/page.tsx +406 -0
  9. package/app/(admin)/statistics/page.tsx +416 -0
  10. package/app/(admin)/tickets/[id]/page.tsx +348 -0
  11. package/app/(admin)/tickets/new/page.tsx +309 -0
  12. package/app/(admin)/tickets/page.tsx +27 -0
  13. package/app/api/audit/route.ts +30 -0
  14. package/app/api/auth/feishu/callback/route.ts +72 -0
  15. package/app/api/auth/feishu/login/route.ts +17 -0
  16. package/app/api/auth/feishu/sso/route.ts +78 -0
  17. package/app/api/auth/login/route.ts +85 -0
  18. package/app/api/auth/oauth/route.ts +168 -0
  19. package/app/api/config/providers/route.ts +105 -0
  20. package/app/api/config/route.ts +115 -0
  21. package/app/api/config/status/route.ts +56 -0
  22. package/app/api/config/test/route.ts +212 -0
  23. package/app/api/documents/[id]/route.ts +88 -0
  24. package/app/api/documents/route.ts +53 -0
  25. package/app/api/health/route.ts +32 -0
  26. package/app/api/knowledge/[id]/route.ts +152 -0
  27. package/app/api/knowledge/from-session/route.ts +27 -0
  28. package/app/api/knowledge/route.ts +100 -0
  29. package/app/api/market/knowledge/[id]/route.ts +92 -0
  30. package/app/api/market/knowledge/route.ts +130 -0
  31. package/app/api/marketplace/skills/[id]/approve/route.ts +68 -0
  32. package/app/api/marketplace/skills/[id]/certify/route.ts +54 -0
  33. package/app/api/marketplace/skills/[id]/install/route.ts +180 -0
  34. package/app/api/marketplace/skills/[id]/promote-to-system/route.ts +219 -0
  35. package/app/api/marketplace/skills/[id]/rate/route.ts +90 -0
  36. package/app/api/marketplace/skills/[id]/ratings/route.ts +55 -0
  37. package/app/api/marketplace/skills/[id]/reject/route.ts +68 -0
  38. package/app/api/marketplace/skills/[id]/route.ts +177 -0
  39. package/app/api/marketplace/skills/route.ts +235 -0
  40. package/app/api/memory/route.ts +40 -0
  41. package/app/api/my/files/[id]/route.ts +52 -0
  42. package/app/api/my/files/route.ts +230 -0
  43. package/app/api/my/knowledge/route.ts +36 -0
  44. package/app/api/pi-chat/route.ts +443 -0
  45. package/app/api/recommend/route.ts +38 -0
  46. package/app/api/scheduled-tasks/[id]/execute/route.ts +132 -0
  47. package/app/api/scheduled-tasks/[id]/route.ts +165 -0
  48. package/app/api/scheduled-tasks/[id]/toggle/route.ts +53 -0
  49. package/app/api/scheduled-tasks/route.ts +101 -0
  50. package/app/api/sessions/[id]/messages/route.ts +212 -0
  51. package/app/api/sessions/route.ts +101 -0
  52. package/app/api/share/file/[id]/route.ts +37 -0
  53. package/app/api/skills/[name]/execute/route.ts +121 -0
  54. package/app/api/skills/[name]/route.ts +167 -0
  55. package/app/api/skills/create/route.ts +65 -0
  56. package/app/api/skills/generate/route.ts +405 -0
  57. package/app/api/skills/installed/route.ts +151 -0
  58. package/app/api/skills/route.ts +174 -0
  59. package/app/api/skills/translate/route.ts +40 -0
  60. package/app/api/skills/user/[name]/route.ts +159 -0
  61. package/app/api/skills/user/route.ts +90 -0
  62. package/app/api/statistics/route.ts +94 -0
  63. package/app/api/task-executions/[id]/route.ts +34 -0
  64. package/app/api/task-executions/route.ts +29 -0
  65. package/app/api/tickets/[id]/approve/route.ts +129 -0
  66. package/app/api/tickets/[id]/execute/route.ts +201 -0
  67. package/app/api/tickets/[id]/route.ts +127 -0
  68. package/app/api/tickets/route.ts +103 -0
  69. package/app/api/user/skills/route.ts +175 -0
  70. package/app/api/users/route.ts +80 -0
  71. package/app/chat/page.tsx +5 -0
  72. package/app/globals.css +84 -0
  73. package/app/h5/layout.tsx +5 -0
  74. package/app/h5/mobile-approvals-page.tsx +167 -0
  75. package/app/h5/mobile-chat-page.tsx +951 -0
  76. package/app/h5/mobile-profile-page.tsx +147 -0
  77. package/app/h5/mobile-tickets-page.tsx +121 -0
  78. package/app/h5/page.tsx +23 -0
  79. package/app/h5/ticket-action-buttons.tsx +80 -0
  80. package/app/layout.tsx +26 -0
  81. package/app/login/page.tsx +318 -0
  82. package/app/market/knowledge/[id]/page.tsx +77 -0
  83. package/app/market/knowledge/page.tsx +358 -0
  84. package/app/market/layout.tsx +29 -0
  85. package/app/market/page.tsx +18 -0
  86. package/app/market/skills/page.tsx +397 -0
  87. package/app/my/files/page.tsx +511 -0
  88. package/app/my/knowledge/[id]/page.tsx +271 -0
  89. package/app/my/knowledge/new/page.tsx +234 -0
  90. package/app/my/knowledge/page.tsx +248 -0
  91. package/app/my/layout.tsx +32 -0
  92. package/app/my/memory/page.tsx +164 -0
  93. package/app/my/page.tsx +18 -0
  94. package/app/my/scheduled-tasks/[id]/edit/page.tsx +290 -0
  95. package/app/my/scheduled-tasks/[id]/executions/page.tsx +275 -0
  96. package/app/my/scheduled-tasks/[id]/page.tsx +284 -0
  97. package/app/my/scheduled-tasks/new/page.tsx +230 -0
  98. package/app/my/scheduled-tasks/page.tsx +27 -0
  99. package/app/my/skills/[name]/page.tsx +320 -0
  100. package/app/my/skills/new/page.tsx +394 -0
  101. package/app/my/skills/page.tsx +303 -0
  102. package/app/page.tsx +2288 -0
  103. package/app/share/[sessionId]/page.tsx +226 -0
  104. package/app/share/file/[id]/page.tsx +140 -0
  105. package/bin/README.md +63 -0
  106. package/bin/generate-api-system +300 -0
  107. package/bin/postinstall.js +95 -0
  108. package/bin/work-agent.js +173 -0
  109. package/components/ai-elements/agent.tsx +142 -0
  110. package/components/ai-elements/artifact.tsx +149 -0
  111. package/components/ai-elements/attachments.tsx +427 -0
  112. package/components/ai-elements/audio-player.tsx +232 -0
  113. package/components/ai-elements/canvas.tsx +26 -0
  114. package/components/ai-elements/chain-of-thought.tsx +223 -0
  115. package/components/ai-elements/checkpoint.tsx +72 -0
  116. package/components/ai-elements/code-block.tsx +555 -0
  117. package/components/ai-elements/commit.tsx +449 -0
  118. package/components/ai-elements/confirmation.tsx +173 -0
  119. package/components/ai-elements/connection.tsx +28 -0
  120. package/components/ai-elements/context.tsx +410 -0
  121. package/components/ai-elements/controls.tsx +19 -0
  122. package/components/ai-elements/conversation.tsx +167 -0
  123. package/components/ai-elements/edge.tsx +144 -0
  124. package/components/ai-elements/environment-variables.tsx +325 -0
  125. package/components/ai-elements/file-tree.tsx +298 -0
  126. package/components/ai-elements/image.tsx +25 -0
  127. package/components/ai-elements/inline-citation.tsx +294 -0
  128. package/components/ai-elements/jsx-preview.tsx +250 -0
  129. package/components/ai-elements/message.tsx +367 -0
  130. package/components/ai-elements/mic-selector.tsx +372 -0
  131. package/components/ai-elements/model-selector.tsx +214 -0
  132. package/components/ai-elements/node.tsx +72 -0
  133. package/components/ai-elements/open-in-chat.tsx +367 -0
  134. package/components/ai-elements/package-info.tsx +235 -0
  135. package/components/ai-elements/panel.tsx +16 -0
  136. package/components/ai-elements/persona.tsx +280 -0
  137. package/components/ai-elements/plan.tsx +144 -0
  138. package/components/ai-elements/prompt-input.tsx +1341 -0
  139. package/components/ai-elements/queue.tsx +275 -0
  140. package/components/ai-elements/reasoning.tsx +355 -0
  141. package/components/ai-elements/sandbox.tsx +133 -0
  142. package/components/ai-elements/schema-display.tsx +473 -0
  143. package/components/ai-elements/shimmer.tsx +78 -0
  144. package/components/ai-elements/snippet.tsx +141 -0
  145. package/components/ai-elements/sources.tsx +78 -0
  146. package/components/ai-elements/speech-input.tsx +324 -0
  147. package/components/ai-elements/stack-trace.tsx +531 -0
  148. package/components/ai-elements/suggestion.tsx +58 -0
  149. package/components/ai-elements/task.tsx +88 -0
  150. package/components/ai-elements/terminal.tsx +277 -0
  151. package/components/ai-elements/test-results.tsx +497 -0
  152. package/components/ai-elements/tool.tsx +174 -0
  153. package/components/ai-elements/toolbar.tsx +17 -0
  154. package/components/ai-elements/transcription.tsx +126 -0
  155. package/components/ai-elements/voice-selector.tsx +525 -0
  156. package/components/ai-elements/web-preview.tsx +282 -0
  157. package/components/audit-log-list.tsx +114 -0
  158. package/components/chat/EmptyPreviewState.tsx +12 -0
  159. package/components/chat/KnowledgePickerDialog.tsx +464 -0
  160. package/components/chat/KnowledgePreview.tsx +70 -0
  161. package/components/chat/KnowledgePreviewPanel.tsx +86 -0
  162. package/components/chat/MentionInput.tsx +309 -0
  163. package/components/chat/OrganizeDialog.tsx +258 -0
  164. package/components/chat/RecommendationBanner.tsx +94 -0
  165. package/components/chat/SaveToKnowledgeDialog.tsx +193 -0
  166. package/components/chat/SkillSelector.tsx +305 -0
  167. package/components/chat/SkillSwitcher.tsx +163 -0
  168. package/components/client-layout.tsx +15 -0
  169. package/components/knowledge/KnowledgeMetadataPanel.tsx +293 -0
  170. package/components/layout-wrapper.tsx +18 -0
  171. package/components/mobile-layout.tsx +62 -0
  172. package/components/scheduled-task-list.tsx +356 -0
  173. package/components/setup-guide.tsx +484 -0
  174. package/components/sub-nav.tsx +54 -0
  175. package/components/ticket-detail-content.tsx +383 -0
  176. package/components/ticket-list.tsx +366 -0
  177. package/components/top-nav.tsx +132 -0
  178. package/components/ui/accordion.tsx +58 -0
  179. package/components/ui/alert.tsx +59 -0
  180. package/components/ui/avatar.tsx +50 -0
  181. package/components/ui/badge.tsx +36 -0
  182. package/components/ui/button-group.tsx +83 -0
  183. package/components/ui/button.tsx +57 -0
  184. package/components/ui/card.tsx +91 -0
  185. package/components/ui/carousel.tsx +262 -0
  186. package/components/ui/collapsible.tsx +11 -0
  187. package/components/ui/command.tsx +153 -0
  188. package/components/ui/dialog.tsx +122 -0
  189. package/components/ui/dropdown-menu.tsx +200 -0
  190. package/components/ui/hover-card.tsx +29 -0
  191. package/components/ui/input-group.tsx +170 -0
  192. package/components/ui/input.tsx +22 -0
  193. package/components/ui/label.tsx +26 -0
  194. package/components/ui/popover.tsx +31 -0
  195. package/components/ui/progress.tsx +28 -0
  196. package/components/ui/scroll-area.tsx +48 -0
  197. package/components/ui/select.tsx +174 -0
  198. package/components/ui/separator.tsx +31 -0
  199. package/components/ui/spinner.tsx +16 -0
  200. package/components/ui/switch.tsx +29 -0
  201. package/components/ui/table.tsx +120 -0
  202. package/components/ui/tabs.tsx +55 -0
  203. package/components/ui/textarea.tsx +22 -0
  204. package/components/ui/tooltip.tsx +30 -0
  205. package/components/welcome-guide.tsx +182 -0
  206. package/components.json +24 -0
  207. package/lib/command-parser.ts +331 -0
  208. package/lib/dangerous-commands.ts +672 -0
  209. package/lib/db.ts +2250 -0
  210. package/lib/feishu-auth.ts +135 -0
  211. package/lib/file-storage.ts +306 -0
  212. package/lib/file-tool.ts +583 -0
  213. package/lib/knowledge-tool.ts +152 -0
  214. package/lib/knowledge-types.ts +66 -0
  215. package/lib/market-client.ts +313 -0
  216. package/lib/market-db.ts +736 -0
  217. package/lib/market-types.ts +51 -0
  218. package/lib/memory-tool.ts +211 -0
  219. package/lib/memory.ts +197 -0
  220. package/lib/pi-config.ts +436 -0
  221. package/lib/pi-session.ts +799 -0
  222. package/lib/pinyin.ts +13 -0
  223. package/lib/recommendation.ts +227 -0
  224. package/lib/risk-estimator.ts +350 -0
  225. package/lib/scheduled-task-tool.ts +184 -0
  226. package/lib/scheduler-init.ts +43 -0
  227. package/lib/scheduler.ts +416 -0
  228. package/lib/secure-bash-tool.ts +413 -0
  229. package/lib/skill-engine.ts +396 -0
  230. package/lib/skill-generator.ts +269 -0
  231. package/lib/skill-loader.ts +234 -0
  232. package/lib/skill-tool.ts +188 -0
  233. package/lib/skill-types.ts +82 -0
  234. package/lib/skills-init.ts +58 -0
  235. package/lib/ticket-tool.ts +246 -0
  236. package/lib/user-skill-types.ts +30 -0
  237. package/lib/user-skills.ts +362 -0
  238. package/lib/utils.ts +6 -0
  239. package/lib/workflow.ts +154 -0
  240. package/lib/zip-tool.ts +191 -0
  241. package/next.config.js +8 -0
  242. package/package.json +106 -0
  243. package/public/.gitkeep +1 -0
  244. package/public/icon.svg +1 -0
  245. package/tsconfig.json +42 -0
@@ -0,0 +1,497 @@
1
+ "use client";
2
+
3
+ import type { ComponentProps, HTMLAttributes } from "react";
4
+
5
+ import { Badge } from "@/components/ui/badge";
6
+ import {
7
+ Collapsible,
8
+ CollapsibleContent,
9
+ CollapsibleTrigger,
10
+ } from "@/components/ui/collapsible";
11
+ import { cn } from "@/lib/utils";
12
+ import {
13
+ CheckCircle2Icon,
14
+ ChevronRightIcon,
15
+ CircleDotIcon,
16
+ CircleIcon,
17
+ XCircleIcon,
18
+ } from "lucide-react";
19
+ import { createContext, useContext, useMemo } from "react";
20
+
21
+ type TestStatus = "passed" | "failed" | "skipped" | "running";
22
+
23
+ interface TestResultsSummary {
24
+ passed: number;
25
+ failed: number;
26
+ skipped: number;
27
+ total: number;
28
+ duration?: number;
29
+ }
30
+
31
+ interface TestResultsContextType {
32
+ summary?: TestResultsSummary;
33
+ }
34
+
35
+ const TestResultsContext = createContext<TestResultsContextType>({});
36
+
37
+ const formatDuration = (ms: number) => {
38
+ if (ms < 1000) {
39
+ return `${ms}ms`;
40
+ }
41
+ return `${(ms / 1000).toFixed(2)}s`;
42
+ };
43
+
44
+ export type TestResultsProps = HTMLAttributes<HTMLDivElement> & {
45
+ summary?: TestResultsSummary;
46
+ };
47
+
48
+ export const TestResults = ({
49
+ summary,
50
+ className,
51
+ children,
52
+ ...props
53
+ }: TestResultsProps) => {
54
+ const contextValue = useMemo(() => ({ summary }), [summary]);
55
+
56
+ return (
57
+ <TestResultsContext.Provider value={contextValue}>
58
+ <div
59
+ className={cn("rounded-lg border bg-background", className)}
60
+ {...props}
61
+ >
62
+ {children ??
63
+ (summary && (
64
+ <TestResultsHeader>
65
+ <TestResultsSummary />
66
+ <TestResultsDuration />
67
+ </TestResultsHeader>
68
+ ))}
69
+ </div>
70
+ </TestResultsContext.Provider>
71
+ );
72
+ };
73
+
74
+ export type TestResultsHeaderProps = HTMLAttributes<HTMLDivElement>;
75
+
76
+ export const TestResultsHeader = ({
77
+ className,
78
+ children,
79
+ ...props
80
+ }: TestResultsHeaderProps) => (
81
+ <div
82
+ className={cn(
83
+ "flex items-center justify-between border-b px-4 py-3",
84
+ className
85
+ )}
86
+ {...props}
87
+ >
88
+ {children}
89
+ </div>
90
+ );
91
+
92
+ export type TestResultsSummaryProps = HTMLAttributes<HTMLDivElement>;
93
+
94
+ export const TestResultsSummary = ({
95
+ className,
96
+ children,
97
+ ...props
98
+ }: TestResultsSummaryProps) => {
99
+ const { summary } = useContext(TestResultsContext);
100
+
101
+ if (!summary) {
102
+ return null;
103
+ }
104
+
105
+ return (
106
+ <div className={cn("flex items-center gap-3", className)} {...props}>
107
+ {children ?? (
108
+ <>
109
+ <Badge
110
+ className="gap-1 bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
111
+ variant="secondary"
112
+ >
113
+ <CheckCircle2Icon className="size-3" />
114
+ {summary.passed} passed
115
+ </Badge>
116
+ {summary.failed > 0 && (
117
+ <Badge
118
+ className="gap-1 bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
119
+ variant="secondary"
120
+ >
121
+ <XCircleIcon className="size-3" />
122
+ {summary.failed} failed
123
+ </Badge>
124
+ )}
125
+ {summary.skipped > 0 && (
126
+ <Badge
127
+ className="gap-1 bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
128
+ variant="secondary"
129
+ >
130
+ <CircleIcon className="size-3" />
131
+ {summary.skipped} skipped
132
+ </Badge>
133
+ )}
134
+ </>
135
+ )}
136
+ </div>
137
+ );
138
+ };
139
+
140
+ export type TestResultsDurationProps = HTMLAttributes<HTMLSpanElement>;
141
+
142
+ export const TestResultsDuration = ({
143
+ className,
144
+ children,
145
+ ...props
146
+ }: TestResultsDurationProps) => {
147
+ const { summary } = useContext(TestResultsContext);
148
+
149
+ if (!summary?.duration) {
150
+ return null;
151
+ }
152
+
153
+ return (
154
+ <span className={cn("text-muted-foreground text-sm", className)} {...props}>
155
+ {children ?? formatDuration(summary.duration)}
156
+ </span>
157
+ );
158
+ };
159
+
160
+ export type TestResultsProgressProps = HTMLAttributes<HTMLDivElement>;
161
+
162
+ export const TestResultsProgress = ({
163
+ className,
164
+ children,
165
+ ...props
166
+ }: TestResultsProgressProps) => {
167
+ const { summary } = useContext(TestResultsContext);
168
+
169
+ if (!summary) {
170
+ return null;
171
+ }
172
+
173
+ const passedPercent = (summary.passed / summary.total) * 100;
174
+ const failedPercent = (summary.failed / summary.total) * 100;
175
+
176
+ return (
177
+ <div className={cn("space-y-2", className)} {...props}>
178
+ {children ?? (
179
+ <>
180
+ <div className="flex h-2 overflow-hidden rounded-full bg-muted">
181
+ <div
182
+ className="bg-green-500 transition-all"
183
+ style={{ width: `${passedPercent}%` }}
184
+ />
185
+ <div
186
+ className="bg-red-500 transition-all"
187
+ style={{ width: `${failedPercent}%` }}
188
+ />
189
+ </div>
190
+ <div className="flex justify-between text-muted-foreground text-xs">
191
+ <span>
192
+ {summary.passed}/{summary.total} tests passed
193
+ </span>
194
+ <span>{passedPercent.toFixed(0)}%</span>
195
+ </div>
196
+ </>
197
+ )}
198
+ </div>
199
+ );
200
+ };
201
+
202
+ export type TestResultsContentProps = HTMLAttributes<HTMLDivElement>;
203
+
204
+ export const TestResultsContent = ({
205
+ className,
206
+ children,
207
+ ...props
208
+ }: TestResultsContentProps) => (
209
+ <div className={cn("space-y-2 p-4", className)} {...props}>
210
+ {children}
211
+ </div>
212
+ );
213
+
214
+ interface TestSuiteContextType {
215
+ name: string;
216
+ status: TestStatus;
217
+ }
218
+
219
+ const TestSuiteContext = createContext<TestSuiteContextType>({
220
+ name: "",
221
+ status: "passed",
222
+ });
223
+
224
+ export type TestSuiteProps = ComponentProps<typeof Collapsible> & {
225
+ name: string;
226
+ status: TestStatus;
227
+ };
228
+
229
+ export const TestSuite = ({
230
+ name,
231
+ status,
232
+ className,
233
+ children,
234
+ ...props
235
+ }: TestSuiteProps) => {
236
+ const contextValue = useMemo(() => ({ name, status }), [name, status]);
237
+
238
+ return (
239
+ <TestSuiteContext.Provider value={contextValue}>
240
+ <Collapsible className={cn("rounded-lg border", className)} {...props}>
241
+ {children}
242
+ </Collapsible>
243
+ </TestSuiteContext.Provider>
244
+ );
245
+ };
246
+
247
+ export type TestSuiteNameProps = ComponentProps<typeof CollapsibleTrigger>;
248
+
249
+ export const TestSuiteName = ({
250
+ className,
251
+ children,
252
+ ...props
253
+ }: TestSuiteNameProps) => {
254
+ const { name, status } = useContext(TestSuiteContext);
255
+
256
+ return (
257
+ <CollapsibleTrigger
258
+ className={cn(
259
+ "group flex w-full items-center gap-2 px-4 py-3 text-left transition-colors hover:bg-muted/50",
260
+ className
261
+ )}
262
+ {...props}
263
+ >
264
+ <ChevronRightIcon className="size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-90" />
265
+ <TestStatusIcon status={status} />
266
+ <span className="font-medium text-sm">{children ?? name}</span>
267
+ </CollapsibleTrigger>
268
+ );
269
+ };
270
+
271
+ export type TestSuiteStatsProps = HTMLAttributes<HTMLDivElement> & {
272
+ passed?: number;
273
+ failed?: number;
274
+ skipped?: number;
275
+ };
276
+
277
+ export const TestSuiteStats = ({
278
+ passed = 0,
279
+ failed = 0,
280
+ skipped = 0,
281
+ className,
282
+ children,
283
+ ...props
284
+ }: TestSuiteStatsProps) => (
285
+ <div
286
+ className={cn("ml-auto flex items-center gap-2 text-xs", className)}
287
+ {...props}
288
+ >
289
+ {children ?? (
290
+ <>
291
+ {passed > 0 && (
292
+ <span className="text-green-600 dark:text-green-400">
293
+ {passed} passed
294
+ </span>
295
+ )}
296
+ {failed > 0 && (
297
+ <span className="text-red-600 dark:text-red-400">
298
+ {failed} failed
299
+ </span>
300
+ )}
301
+ {skipped > 0 && (
302
+ <span className="text-yellow-600 dark:text-yellow-400">
303
+ {skipped} skipped
304
+ </span>
305
+ )}
306
+ </>
307
+ )}
308
+ </div>
309
+ );
310
+
311
+ export type TestSuiteContentProps = ComponentProps<typeof CollapsibleContent>;
312
+
313
+ export const TestSuiteContent = ({
314
+ className,
315
+ children,
316
+ ...props
317
+ }: TestSuiteContentProps) => (
318
+ <CollapsibleContent className={cn("border-t", className)} {...props}>
319
+ <div className="divide-y">{children}</div>
320
+ </CollapsibleContent>
321
+ );
322
+
323
+ interface TestContextType {
324
+ name: string;
325
+ status: TestStatus;
326
+ duration?: number;
327
+ }
328
+
329
+ const TestContext = createContext<TestContextType>({
330
+ name: "",
331
+ status: "passed",
332
+ });
333
+
334
+ export type TestProps = HTMLAttributes<HTMLDivElement> & {
335
+ name: string;
336
+ status: TestStatus;
337
+ duration?: number;
338
+ };
339
+
340
+ export const Test = ({
341
+ name,
342
+ status,
343
+ duration,
344
+ className,
345
+ children,
346
+ ...props
347
+ }: TestProps) => {
348
+ const contextValue = useMemo(
349
+ () => ({ duration, name, status }),
350
+ [duration, name, status]
351
+ );
352
+
353
+ return (
354
+ <TestContext.Provider value={contextValue}>
355
+ <div
356
+ className={cn("flex items-center gap-2 px-4 py-2 text-sm", className)}
357
+ {...props}
358
+ >
359
+ {children ?? (
360
+ <>
361
+ <TestStatus />
362
+ <TestName />
363
+ {duration !== undefined && <TestDuration />}
364
+ </>
365
+ )}
366
+ </div>
367
+ </TestContext.Provider>
368
+ );
369
+ };
370
+
371
+ const statusStyles: Record<TestStatus, string> = {
372
+ failed: "text-red-600 dark:text-red-400",
373
+ passed: "text-green-600 dark:text-green-400",
374
+ running: "text-blue-600 dark:text-blue-400",
375
+ skipped: "text-yellow-600 dark:text-yellow-400",
376
+ };
377
+
378
+ const statusIcons: Record<TestStatus, React.ReactNode> = {
379
+ failed: <XCircleIcon className="size-4" />,
380
+ passed: <CheckCircle2Icon className="size-4" />,
381
+ running: <CircleDotIcon className="size-4 animate-pulse" />,
382
+ skipped: <CircleIcon className="size-4" />,
383
+ };
384
+
385
+ const TestStatusIcon = ({ status }: { status: TestStatus }) => (
386
+ <span className={cn("shrink-0", statusStyles[status])}>
387
+ {statusIcons[status]}
388
+ </span>
389
+ );
390
+
391
+ export type TestStatusProps = HTMLAttributes<HTMLSpanElement>;
392
+
393
+ export const TestStatus = ({
394
+ className,
395
+ children,
396
+ ...props
397
+ }: TestStatusProps) => {
398
+ const { status } = useContext(TestContext);
399
+
400
+ return (
401
+ <span
402
+ className={cn("shrink-0", statusStyles[status], className)}
403
+ {...props}
404
+ >
405
+ {children ?? statusIcons[status]}
406
+ </span>
407
+ );
408
+ };
409
+
410
+ export type TestNameProps = HTMLAttributes<HTMLSpanElement>;
411
+
412
+ export const TestName = ({ className, children, ...props }: TestNameProps) => {
413
+ const { name } = useContext(TestContext);
414
+
415
+ return (
416
+ <span className={cn("flex-1", className)} {...props}>
417
+ {children ?? name}
418
+ </span>
419
+ );
420
+ };
421
+
422
+ export type TestDurationProps = HTMLAttributes<HTMLSpanElement>;
423
+
424
+ export const TestDuration = ({
425
+ className,
426
+ children,
427
+ ...props
428
+ }: TestDurationProps) => {
429
+ const { duration } = useContext(TestContext);
430
+
431
+ if (duration === undefined) {
432
+ return null;
433
+ }
434
+
435
+ return (
436
+ <span
437
+ className={cn("ml-auto text-muted-foreground text-xs", className)}
438
+ {...props}
439
+ >
440
+ {children ?? `${duration}ms`}
441
+ </span>
442
+ );
443
+ };
444
+
445
+ export type TestErrorProps = HTMLAttributes<HTMLDivElement>;
446
+
447
+ export const TestError = ({
448
+ className,
449
+ children,
450
+ ...props
451
+ }: TestErrorProps) => (
452
+ <div
453
+ className={cn(
454
+ "mt-2 rounded-md bg-red-50 p-3 dark:bg-red-900/20",
455
+ className
456
+ )}
457
+ {...props}
458
+ >
459
+ {children}
460
+ </div>
461
+ );
462
+
463
+ export type TestErrorMessageProps = HTMLAttributes<HTMLParagraphElement>;
464
+
465
+ export const TestErrorMessage = ({
466
+ className,
467
+ children,
468
+ ...props
469
+ }: TestErrorMessageProps) => (
470
+ <p
471
+ className={cn(
472
+ "font-medium text-red-700 text-sm dark:text-red-400",
473
+ className
474
+ )}
475
+ {...props}
476
+ >
477
+ {children}
478
+ </p>
479
+ );
480
+
481
+ export type TestErrorStackProps = HTMLAttributes<HTMLPreElement>;
482
+
483
+ export const TestErrorStack = ({
484
+ className,
485
+ children,
486
+ ...props
487
+ }: TestErrorStackProps) => (
488
+ <pre
489
+ className={cn(
490
+ "mt-2 overflow-auto font-mono text-red-600 text-xs dark:text-red-400",
491
+ className
492
+ )}
493
+ {...props}
494
+ >
495
+ {children}
496
+ </pre>
497
+ );
@@ -0,0 +1,174 @@
1
+ "use client";
2
+
3
+ import type { DynamicToolUIPart, ToolUIPart } from "ai";
4
+ import type { ComponentProps, ReactNode } from "react";
5
+
6
+ import { Badge } from "@/components/ui/badge";
7
+ import {
8
+ Collapsible,
9
+ CollapsibleContent,
10
+ CollapsibleTrigger,
11
+ } from "@/components/ui/collapsible";
12
+ import { cn } from "@/lib/utils";
13
+ import {
14
+ CheckCircleIcon,
15
+ ChevronDownIcon,
16
+ CircleIcon,
17
+ ClockIcon,
18
+ WrenchIcon,
19
+ XCircleIcon,
20
+ } from "lucide-react";
21
+ import { isValidElement } from "react";
22
+
23
+ import { CodeBlock } from "./code-block";
24
+
25
+ export type ToolProps = ComponentProps<typeof Collapsible>;
26
+
27
+ export const Tool = ({ className, ...props }: ToolProps) => (
28
+ <Collapsible
29
+ className={cn("group not-prose mb-4 w-full rounded-md border", className)}
30
+ {...props}
31
+ />
32
+ );
33
+
34
+ export type ToolPart = ToolUIPart | DynamicToolUIPart;
35
+
36
+ export type ToolHeaderProps = {
37
+ title?: string;
38
+ className?: string;
39
+ } & (
40
+ | { type: ToolUIPart["type"]; state: ToolUIPart["state"]; toolName?: never }
41
+ | {
42
+ type: DynamicToolUIPart["type"];
43
+ state: DynamicToolUIPart["state"];
44
+ toolName: string;
45
+ }
46
+ );
47
+
48
+ const statusLabels: Record<ToolPart["state"], string> = {
49
+ "approval-requested": "Awaiting Approval",
50
+ "approval-responded": "Responded",
51
+ "input-available": "Running",
52
+ "input-streaming": "Pending",
53
+ "output-available": "Completed",
54
+ "output-denied": "Denied",
55
+ "output-error": "Error",
56
+ };
57
+
58
+ const statusIcons: Record<ToolPart["state"], ReactNode> = {
59
+ "approval-requested": <ClockIcon className="size-4 text-yellow-600" />,
60
+ "approval-responded": <CheckCircleIcon className="size-4 text-blue-600" />,
61
+ "input-available": <ClockIcon className="size-4 animate-pulse" />,
62
+ "input-streaming": <CircleIcon className="size-4" />,
63
+ "output-available": <CheckCircleIcon className="size-4 text-green-600" />,
64
+ "output-denied": <XCircleIcon className="size-4 text-orange-600" />,
65
+ "output-error": <XCircleIcon className="size-4 text-red-600" />,
66
+ };
67
+
68
+ export const getStatusBadge = (status: ToolPart["state"]) => (
69
+ <Badge className="gap-1.5 rounded-full text-xs" variant="secondary">
70
+ {statusIcons[status]}
71
+ {statusLabels[status]}
72
+ </Badge>
73
+ );
74
+
75
+ export const ToolHeader = ({
76
+ className,
77
+ title,
78
+ type,
79
+ state,
80
+ toolName,
81
+ ...props
82
+ }: ToolHeaderProps) => {
83
+ const derivedName =
84
+ type === "dynamic-tool" ? toolName : type.split("-").slice(1).join("-");
85
+
86
+ return (
87
+ <CollapsibleTrigger
88
+ className={cn(
89
+ "flex w-full items-center justify-between gap-4 p-3",
90
+ className
91
+ )}
92
+ {...props}
93
+ >
94
+ <div className="flex items-center gap-2">
95
+ <WrenchIcon className="size-4 text-muted-foreground" />
96
+ <span className="font-medium text-sm">{title ?? derivedName}</span>
97
+ {getStatusBadge(state)}
98
+ </div>
99
+ <ChevronDownIcon className="size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
100
+ </CollapsibleTrigger>
101
+ );
102
+ };
103
+
104
+ export type ToolContentProps = ComponentProps<typeof CollapsibleContent>;
105
+
106
+ export const ToolContent = ({ className, ...props }: ToolContentProps) => (
107
+ <CollapsibleContent
108
+ className={cn(
109
+ "data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 space-y-4 p-4 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
110
+ className
111
+ )}
112
+ {...props}
113
+ />
114
+ );
115
+
116
+ export type ToolInputProps = ComponentProps<"div"> & {
117
+ input: ToolPart["input"];
118
+ };
119
+
120
+ export const ToolInput = ({ className, input, ...props }: ToolInputProps) => (
121
+ <div className={cn("space-y-2 overflow-hidden", className)} {...props}>
122
+ <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
123
+ Parameters
124
+ </h4>
125
+ <div className="rounded-md bg-muted/50">
126
+ <CodeBlock code={JSON.stringify(input, null, 2)} language="json" />
127
+ </div>
128
+ </div>
129
+ );
130
+
131
+ export type ToolOutputProps = ComponentProps<"div"> & {
132
+ output: ToolPart["output"];
133
+ errorText: ToolPart["errorText"];
134
+ };
135
+
136
+ export const ToolOutput = ({
137
+ className,
138
+ output,
139
+ errorText,
140
+ ...props
141
+ }: ToolOutputProps) => {
142
+ if (!(output || errorText)) {
143
+ return null;
144
+ }
145
+
146
+ let Output = <div>{output as ReactNode}</div>;
147
+
148
+ if (typeof output === "object" && !isValidElement(output)) {
149
+ Output = (
150
+ <CodeBlock code={JSON.stringify(output, null, 2)} language="json" />
151
+ );
152
+ } else if (typeof output === "string") {
153
+ Output = <CodeBlock code={output} language="json" />;
154
+ }
155
+
156
+ return (
157
+ <div className={cn("space-y-2", className)} {...props}>
158
+ <h4 className="font-medium text-muted-foreground text-xs uppercase tracking-wide">
159
+ {errorText ? "Error" : "Result"}
160
+ </h4>
161
+ <div
162
+ className={cn(
163
+ "overflow-x-auto rounded-md text-xs [&_table]:w-full",
164
+ errorText
165
+ ? "bg-destructive/10 text-destructive"
166
+ : "bg-muted/50 text-foreground"
167
+ )}
168
+ >
169
+ {errorText && <div>{errorText}</div>}
170
+ {Output}
171
+ </div>
172
+ </div>
173
+ );
174
+ };
@@ -0,0 +1,17 @@
1
+ import type { ComponentProps } from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import { NodeToolbar, Position } from "@xyflow/react";
5
+
6
+ type ToolbarProps = ComponentProps<typeof NodeToolbar>;
7
+
8
+ export const Toolbar = ({ className, ...props }: ToolbarProps) => (
9
+ <NodeToolbar
10
+ className={cn(
11
+ "flex items-center gap-1 rounded-sm border bg-background p-1.5",
12
+ className
13
+ )}
14
+ position={Position.Bottom}
15
+ {...props}
16
+ />
17
+ );