@cline/ui 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.
@@ -0,0 +1,776 @@
1
+ "use client";
2
+
3
+ import {
4
+ type ButtonHTMLAttributes,
5
+ createContext,
6
+ forwardRef,
7
+ type HTMLAttributes,
8
+ type MouseEvent as ReactMouseEvent,
9
+ type ReactNode,
10
+ type Ref,
11
+ type RefCallback,
12
+ useCallback,
13
+ useContext,
14
+ useEffect,
15
+ useId,
16
+ useLayoutEffect,
17
+ useMemo,
18
+ useRef,
19
+ useState,
20
+ } from "react";
21
+
22
+ const STICK_TO_BOTTOM_THRESHOLD_PX = 24;
23
+ const SCROLL_BUTTON_THRESHOLD_PX = 120;
24
+
25
+ function classNames(...values: Array<string | undefined | false>): string {
26
+ return values.filter(Boolean).join(" ");
27
+ }
28
+
29
+ function assignRef<T>(ref: Ref<T> | undefined, value: T | null): void {
30
+ if (typeof ref === "function") {
31
+ ref(value);
32
+ return;
33
+ }
34
+ if (ref) {
35
+ ref.current = value;
36
+ }
37
+ }
38
+
39
+ type ConversationContextValue = {
40
+ setContent: (element: HTMLDivElement | null) => void;
41
+ setViewport: (element: HTMLDivElement | null) => void;
42
+ showScrollButton: boolean;
43
+ scrollToBottom: (behavior?: ScrollBehavior) => void;
44
+ };
45
+
46
+ const ConversationContext = createContext<ConversationContextValue | null>(
47
+ null,
48
+ );
49
+
50
+ function useConversation(): ConversationContextValue {
51
+ const context = useContext(ConversationContext);
52
+ if (!context) {
53
+ throw new Error(
54
+ "Conversation components must be rendered inside Conversation",
55
+ );
56
+ }
57
+ return context;
58
+ }
59
+
60
+ export type ConversationProps = HTMLAttributes<HTMLDivElement>;
61
+
62
+ export const Conversation = forwardRef<HTMLDivElement, ConversationProps>(
63
+ ({ children, className, ...props }, ref) => {
64
+ const [viewport, setViewport] = useState<HTMLDivElement | null>(null);
65
+ const [content, setContent] = useState<HTMLDivElement | null>(null);
66
+ const [showScrollButton, setShowScrollButton] = useState(false);
67
+ const shouldStickToBottom = useRef(true);
68
+ const isProgrammaticScroll = useRef(false);
69
+ const lastProgrammaticScrollTop = useRef(0);
70
+ const programmaticScrollTimer = useRef<number | null>(null);
71
+
72
+ const clearProgrammaticScroll = useCallback(() => {
73
+ if (programmaticScrollTimer.current !== null) {
74
+ window.clearTimeout(programmaticScrollTimer.current);
75
+ programmaticScrollTimer.current = null;
76
+ }
77
+ }, []);
78
+
79
+ const updateScrollPosition = useCallback(() => {
80
+ if (!viewport) return;
81
+ const distance =
82
+ viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
83
+ if (isProgrammaticScroll.current) {
84
+ if (viewport.scrollTop + 1 < lastProgrammaticScrollTop.current) {
85
+ isProgrammaticScroll.current = false;
86
+ clearProgrammaticScroll();
87
+ } else {
88
+ lastProgrammaticScrollTop.current = viewport.scrollTop;
89
+ shouldStickToBottom.current = true;
90
+ setShowScrollButton(false);
91
+ if (distance <= STICK_TO_BOTTOM_THRESHOLD_PX) {
92
+ isProgrammaticScroll.current = false;
93
+ clearProgrammaticScroll();
94
+ }
95
+ return;
96
+ }
97
+ }
98
+ shouldStickToBottom.current = distance <= STICK_TO_BOTTOM_THRESHOLD_PX;
99
+ setShowScrollButton(distance > SCROLL_BUTTON_THRESHOLD_PX);
100
+ }, [clearProgrammaticScroll, viewport]);
101
+
102
+ const scrollToBottom = useCallback(
103
+ (behavior: ScrollBehavior = "smooth") => {
104
+ if (!viewport) return;
105
+ clearProgrammaticScroll();
106
+ const prefersReducedMotion =
107
+ behavior === "smooth" &&
108
+ typeof window.matchMedia === "function" &&
109
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
110
+ const effectiveBehavior = prefersReducedMotion ? "auto" : behavior;
111
+ const isSmooth = effectiveBehavior === "smooth";
112
+ isProgrammaticScroll.current = isSmooth;
113
+ lastProgrammaticScrollTop.current = viewport.scrollTop;
114
+ shouldStickToBottom.current = true;
115
+ viewport.scrollTo({
116
+ top: viewport.scrollHeight,
117
+ behavior: effectiveBehavior,
118
+ });
119
+ setShowScrollButton(false);
120
+ if (!isSmooth) return;
121
+ programmaticScrollTimer.current = window.setTimeout(() => {
122
+ isProgrammaticScroll.current = false;
123
+ programmaticScrollTimer.current = null;
124
+ updateScrollPosition();
125
+ }, 1500);
126
+ },
127
+ [clearProgrammaticScroll, updateScrollPosition, viewport],
128
+ );
129
+
130
+ useEffect(() => {
131
+ if (!viewport) return;
132
+ updateScrollPosition();
133
+ viewport.addEventListener("scroll", updateScrollPosition);
134
+ const cancelProgrammaticScroll = () => {
135
+ if (!isProgrammaticScroll.current) return;
136
+ isProgrammaticScroll.current = false;
137
+ clearProgrammaticScroll();
138
+ updateScrollPosition();
139
+ };
140
+ viewport.addEventListener("touchstart", cancelProgrammaticScroll, {
141
+ passive: true,
142
+ });
143
+ viewport.addEventListener("pointerdown", cancelProgrammaticScroll, {
144
+ passive: true,
145
+ });
146
+ const cancelProgrammaticScrollOnKeydown = (event: KeyboardEvent) => {
147
+ if (
148
+ [
149
+ "ArrowDown",
150
+ "ArrowUp",
151
+ "End",
152
+ "Home",
153
+ "PageDown",
154
+ "PageUp",
155
+ " ",
156
+ ].includes(event.key)
157
+ ) {
158
+ cancelProgrammaticScroll();
159
+ }
160
+ };
161
+ viewport.addEventListener("keydown", cancelProgrammaticScrollOnKeydown);
162
+ viewport.addEventListener("wheel", cancelProgrammaticScroll, {
163
+ passive: true,
164
+ });
165
+ return () => {
166
+ viewport.removeEventListener("scroll", updateScrollPosition);
167
+ viewport.removeEventListener("touchstart", cancelProgrammaticScroll);
168
+ viewport.removeEventListener("pointerdown", cancelProgrammaticScroll);
169
+ viewport.removeEventListener(
170
+ "keydown",
171
+ cancelProgrammaticScrollOnKeydown,
172
+ );
173
+ viewport.removeEventListener("wheel", cancelProgrammaticScroll);
174
+ };
175
+ }, [clearProgrammaticScroll, updateScrollPosition, viewport]);
176
+
177
+ useEffect(() => () => clearProgrammaticScroll(), [clearProgrammaticScroll]);
178
+
179
+ useLayoutEffect(() => {
180
+ if (!viewport || !content) return;
181
+ scrollToBottom("auto");
182
+ }, [content, scrollToBottom, viewport]);
183
+
184
+ useEffect(() => {
185
+ if (!content || !viewport || typeof ResizeObserver === "undefined")
186
+ return;
187
+ const observer = new ResizeObserver(() => {
188
+ if (shouldStickToBottom.current) {
189
+ scrollToBottom("auto");
190
+ } else {
191
+ updateScrollPosition();
192
+ }
193
+ });
194
+ observer.observe(content);
195
+ observer.observe(viewport);
196
+ return () => observer.disconnect();
197
+ }, [content, scrollToBottom, updateScrollPosition, viewport]);
198
+
199
+ const value = useMemo<ConversationContextValue>(
200
+ () => ({
201
+ scrollToBottom,
202
+ setContent,
203
+ setViewport,
204
+ showScrollButton,
205
+ }),
206
+ [scrollToBottom, showScrollButton],
207
+ );
208
+
209
+ return (
210
+ <ConversationContext.Provider value={value}>
211
+ <div
212
+ className={classNames("cline-chat-conversation", className)}
213
+ ref={ref}
214
+ {...props}
215
+ >
216
+ {children}
217
+ </div>
218
+ </ConversationContext.Provider>
219
+ );
220
+ },
221
+ );
222
+
223
+ Conversation.displayName = "Conversation";
224
+
225
+ export type ConversationViewportProps = Omit<
226
+ HTMLAttributes<HTMLDivElement>,
227
+ "role"
228
+ >;
229
+
230
+ export const ConversationViewport = forwardRef<
231
+ HTMLDivElement,
232
+ ConversationViewportProps
233
+ >(
234
+ (
235
+ {
236
+ "aria-label": ariaLabel = "Agent conversation",
237
+ "aria-live": ariaLive = "polite",
238
+ className,
239
+ tabIndex = 0,
240
+ ...props
241
+ },
242
+ forwardedRef,
243
+ ) => {
244
+ const { setViewport } = useConversation();
245
+ const ref = useCallback<RefCallback<HTMLDivElement>>(
246
+ (element) => {
247
+ setViewport(element);
248
+ assignRef(forwardedRef, element);
249
+ },
250
+ [forwardedRef, setViewport],
251
+ );
252
+
253
+ return (
254
+ <div
255
+ {...props}
256
+ aria-label={ariaLabel}
257
+ aria-live={ariaLive}
258
+ className={classNames("cline-chat-conversation-viewport", className)}
259
+ ref={ref}
260
+ role="log"
261
+ tabIndex={tabIndex}
262
+ />
263
+ );
264
+ },
265
+ );
266
+
267
+ ConversationViewport.displayName = "ConversationViewport";
268
+
269
+ export type ConversationContentProps = HTMLAttributes<HTMLDivElement>;
270
+
271
+ export const ConversationContent = forwardRef<
272
+ HTMLDivElement,
273
+ ConversationContentProps
274
+ >(({ className, ...props }, forwardedRef) => {
275
+ const { setContent } = useConversation();
276
+ const ref = useCallback<RefCallback<HTMLDivElement>>(
277
+ (element) => {
278
+ setContent(element);
279
+ assignRef(forwardedRef, element);
280
+ },
281
+ [forwardedRef, setContent],
282
+ );
283
+
284
+ return (
285
+ <div
286
+ className={classNames("cline-chat-conversation-content", className)}
287
+ ref={ref}
288
+ {...props}
289
+ />
290
+ );
291
+ });
292
+
293
+ ConversationContent.displayName = "ConversationContent";
294
+
295
+ export type ConversationEmptyStateProps = HTMLAttributes<HTMLDivElement> & {
296
+ title?: string;
297
+ description?: string;
298
+ icon?: ReactNode;
299
+ };
300
+
301
+ export const ConversationEmptyState = ({
302
+ children,
303
+ className,
304
+ description = "Start a conversation to see messages here.",
305
+ icon,
306
+ title = "No messages yet",
307
+ ...props
308
+ }: ConversationEmptyStateProps) => (
309
+ <div className={classNames("cline-chat-empty-state", className)} {...props}>
310
+ {children ?? (
311
+ <>
312
+ {icon ? (
313
+ <div className="cline-chat-empty-state-icon">{icon}</div>
314
+ ) : null}
315
+ <div>
316
+ <h3>{title}</h3>
317
+ {description ? <p>{description}</p> : null}
318
+ </div>
319
+ </>
320
+ )}
321
+ </div>
322
+ );
323
+
324
+ export type ConversationScrollButtonProps = Omit<
325
+ ButtonHTMLAttributes<HTMLButtonElement>,
326
+ "type"
327
+ >;
328
+
329
+ export const ConversationScrollButton = ({
330
+ "aria-label": ariaLabel = "Scroll to latest message",
331
+ children,
332
+ className,
333
+ onClick,
334
+ ...props
335
+ }: ConversationScrollButtonProps) => {
336
+ const { scrollToBottom, showScrollButton } = useConversation();
337
+ if (!showScrollButton) return null;
338
+
339
+ return (
340
+ <button
341
+ {...props}
342
+ aria-label={ariaLabel}
343
+ className={classNames("cline-chat-scroll-button", className)}
344
+ onClick={(event) => {
345
+ onClick?.(event);
346
+ if (!event.defaultPrevented) scrollToBottom();
347
+ }}
348
+ type="button"
349
+ >
350
+ {children ?? <ChevronDownIcon />}
351
+ </button>
352
+ );
353
+ };
354
+
355
+ export type AgentMessageRole =
356
+ | "user"
357
+ | "assistant"
358
+ | "system"
359
+ | "status"
360
+ | "error";
361
+
362
+ export type MessageProps = HTMLAttributes<HTMLDivElement> & {
363
+ from: AgentMessageRole;
364
+ };
365
+
366
+ export const Message = ({ className, from, ...props }: MessageProps) => (
367
+ <div
368
+ {...props}
369
+ className={classNames("cline-chat-message", className)}
370
+ data-role={from}
371
+ />
372
+ );
373
+
374
+ export type MessageContentProps = HTMLAttributes<HTMLDivElement>;
375
+
376
+ export const MessageContent = ({
377
+ className,
378
+ ...props
379
+ }: MessageContentProps) => (
380
+ <div
381
+ className={classNames("cline-chat-message-content", className)}
382
+ {...props}
383
+ />
384
+ );
385
+
386
+ export type MessageActionsProps = HTMLAttributes<HTMLDivElement> & {
387
+ visible?: boolean;
388
+ };
389
+
390
+ export const MessageActions = ({
391
+ className,
392
+ visible = false,
393
+ ...props
394
+ }: MessageActionsProps) => (
395
+ <div
396
+ {...props}
397
+ className={classNames("cline-chat-message-actions", className)}
398
+ data-visible={visible || undefined}
399
+ />
400
+ );
401
+
402
+ export type MessageActionProps = Omit<
403
+ ButtonHTMLAttributes<HTMLButtonElement>,
404
+ "type"
405
+ > & {
406
+ label: string;
407
+ };
408
+
409
+ export const MessageAction = ({
410
+ "aria-label": ariaLabel,
411
+ className,
412
+ label,
413
+ ...props
414
+ }: MessageActionProps) => (
415
+ <button
416
+ {...props}
417
+ aria-label={ariaLabel ?? label}
418
+ className={classNames("cline-chat-message-action", className)}
419
+ type="button"
420
+ />
421
+ );
422
+
423
+ type DisclosureState = {
424
+ isOpen: boolean;
425
+ panelId: string;
426
+ setIsOpen: (open: boolean) => void;
427
+ };
428
+
429
+ type ReasoningContextValue = DisclosureState & {
430
+ isStreaming: boolean;
431
+ };
432
+
433
+ const ReasoningContext = createContext<ReasoningContextValue | null>(null);
434
+
435
+ function useReasoning(): ReasoningContextValue {
436
+ const context = useContext(ReasoningContext);
437
+ if (!context) {
438
+ throw new Error("Reasoning components must be rendered inside Reasoning");
439
+ }
440
+ return context;
441
+ }
442
+
443
+ export type ReasoningProps = Omit<
444
+ HTMLAttributes<HTMLDivElement>,
445
+ "onChange"
446
+ > & {
447
+ isStreaming?: boolean;
448
+ open?: boolean;
449
+ defaultOpen?: boolean;
450
+ onOpenChange?: (open: boolean) => void;
451
+ };
452
+
453
+ export const Reasoning = ({
454
+ className,
455
+ defaultOpen = false,
456
+ isStreaming = false,
457
+ onOpenChange,
458
+ open,
459
+ ...props
460
+ }: ReasoningProps) => {
461
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
462
+ const panelId = useId();
463
+ const isOpen = open ?? internalOpen;
464
+ const setIsOpen = useCallback(
465
+ (nextOpen: boolean) => {
466
+ if (open === undefined) setInternalOpen(nextOpen);
467
+ onOpenChange?.(nextOpen);
468
+ },
469
+ [onOpenChange, open],
470
+ );
471
+ const value = useMemo(
472
+ () => ({ isOpen, isStreaming, panelId, setIsOpen }),
473
+ [isOpen, isStreaming, panelId, setIsOpen],
474
+ );
475
+
476
+ return (
477
+ <ReasoningContext.Provider value={value}>
478
+ <div
479
+ {...props}
480
+ className={classNames("cline-chat-reasoning", className)}
481
+ data-streaming={isStreaming || undefined}
482
+ />
483
+ </ReasoningContext.Provider>
484
+ );
485
+ };
486
+
487
+ export type ReasoningTriggerProps = Omit<
488
+ ButtonHTMLAttributes<HTMLButtonElement>,
489
+ "aria-controls" | "aria-expanded" | "type"
490
+ > & {
491
+ completeLabel?: string;
492
+ streamingLabel?: string;
493
+ };
494
+
495
+ export const ReasoningTrigger = ({
496
+ children,
497
+ className,
498
+ completeLabel = "Thought process",
499
+ onClick,
500
+ streamingLabel = "Thinking",
501
+ ...props
502
+ }: ReasoningTriggerProps) => {
503
+ const { isOpen, isStreaming, panelId, setIsOpen } = useReasoning();
504
+ return (
505
+ <button
506
+ {...props}
507
+ aria-controls={panelId}
508
+ aria-expanded={isOpen}
509
+ className={classNames("cline-chat-reasoning-trigger", className)}
510
+ onClick={(event) => {
511
+ onClick?.(event);
512
+ if (!event.defaultPrevented) setIsOpen(!isOpen);
513
+ }}
514
+ type="button"
515
+ >
516
+ {children ?? (
517
+ <>
518
+ <BrainIcon />
519
+ <span>{isStreaming ? streamingLabel : completeLabel}</span>
520
+ <span aria-live="polite" className="cline-chat-reasoning-status">
521
+ {isStreaming ? "In progress" : "Complete"}
522
+ </span>
523
+ <ChevronDownIcon className="cline-chat-disclosure-icon" />
524
+ </>
525
+ )}
526
+ </button>
527
+ );
528
+ };
529
+
530
+ export type ReasoningContentProps = Omit<
531
+ HTMLAttributes<HTMLDivElement>,
532
+ "hidden" | "id"
533
+ >;
534
+
535
+ export const ReasoningContent = ({
536
+ className,
537
+ ...props
538
+ }: ReasoningContentProps) => {
539
+ const { isOpen, panelId } = useReasoning();
540
+ if (!isOpen) return null;
541
+ return (
542
+ <div
543
+ {...props}
544
+ className={classNames("cline-chat-reasoning-content", className)}
545
+ id={panelId}
546
+ />
547
+ );
548
+ };
549
+
550
+ export type ToolActivityStatus = "pending" | "running" | "success" | "error";
551
+
552
+ type ToolActivityContextValue = DisclosureState & {
553
+ expandable: boolean;
554
+ };
555
+
556
+ const ToolActivityContext = createContext<ToolActivityContextValue | null>(
557
+ null,
558
+ );
559
+
560
+ function useToolActivity(): ToolActivityContextValue {
561
+ const context = useContext(ToolActivityContext);
562
+ if (!context) {
563
+ throw new Error(
564
+ "ToolActivity components must be rendered inside ToolActivity",
565
+ );
566
+ }
567
+ return context;
568
+ }
569
+
570
+ export type ToolActivityProps = Omit<
571
+ HTMLAttributes<HTMLDivElement>,
572
+ "onChange"
573
+ > & {
574
+ expandable?: boolean;
575
+ open?: boolean;
576
+ defaultOpen?: boolean;
577
+ onOpenChange?: (open: boolean) => void;
578
+ };
579
+
580
+ export const ToolActivity = ({
581
+ className,
582
+ defaultOpen = false,
583
+ expandable = true,
584
+ onOpenChange,
585
+ open,
586
+ ...props
587
+ }: ToolActivityProps) => {
588
+ const [internalOpen, setInternalOpen] = useState(defaultOpen);
589
+ const panelId = useId();
590
+ const isOpen = expandable && (open ?? internalOpen);
591
+ const setIsOpen = useCallback(
592
+ (nextOpen: boolean) => {
593
+ if (!expandable) return;
594
+ if (open === undefined) setInternalOpen(nextOpen);
595
+ onOpenChange?.(nextOpen);
596
+ },
597
+ [expandable, onOpenChange, open],
598
+ );
599
+ const value = useMemo(
600
+ () => ({ expandable, isOpen, panelId, setIsOpen }),
601
+ [expandable, isOpen, panelId, setIsOpen],
602
+ );
603
+
604
+ return (
605
+ <ToolActivityContext.Provider value={value}>
606
+ <div
607
+ {...props}
608
+ className={classNames("cline-chat-tool", className)}
609
+ data-expandable={expandable || undefined}
610
+ />
611
+ </ToolActivityContext.Provider>
612
+ );
613
+ };
614
+
615
+ export type ToolActivityTriggerProps = Omit<
616
+ HTMLAttributes<HTMLElement>,
617
+ "aria-controls" | "aria-expanded"
618
+ > & {
619
+ icon?: ReactNode;
620
+ label: ReactNode;
621
+ status?: ToolActivityStatus;
622
+ additions?: number;
623
+ deletions?: number;
624
+ disabled?: boolean;
625
+ };
626
+
627
+ export const ToolActivityTrigger = ({
628
+ additions,
629
+ children,
630
+ className,
631
+ deletions,
632
+ disabled = false,
633
+ icon,
634
+ label,
635
+ onClick,
636
+ status = "success",
637
+ ...props
638
+ }: ToolActivityTriggerProps) => {
639
+ const { expandable, isOpen, panelId, setIsOpen } = useToolActivity();
640
+ const content = children ?? (
641
+ <>
642
+ {icon ? <span className="cline-chat-tool-icon">{icon}</span> : null}
643
+ <span className="cline-chat-tool-label">{label}</span>
644
+ {additions !== undefined || deletions !== undefined ? (
645
+ <span className="cline-chat-tool-diff">
646
+ {additions !== undefined ? (
647
+ <span data-diff="additions">+{additions}</span>
648
+ ) : null}{" "}
649
+ {deletions !== undefined ? (
650
+ <span data-diff="deletions">-{deletions}</span>
651
+ ) : null}
652
+ </span>
653
+ ) : null}
654
+ {status === "running" || status === "pending" ? (
655
+ <output aria-label={status} className="cline-chat-tool-progress" />
656
+ ) : null}
657
+ {expandable ? (
658
+ <ChevronDownIcon className="cline-chat-disclosure-icon" />
659
+ ) : null}
660
+ </>
661
+ );
662
+ const handleClick = (event: ReactMouseEvent<HTMLElement>) => {
663
+ onClick?.(event);
664
+ if (expandable && !event.defaultPrevented) setIsOpen(!isOpen);
665
+ };
666
+ const triggerClassName = classNames("cline-chat-tool-trigger", className);
667
+
668
+ if (expandable) {
669
+ return (
670
+ <button
671
+ {...(props as ButtonHTMLAttributes<HTMLButtonElement>)}
672
+ aria-controls={panelId}
673
+ aria-expanded={isOpen}
674
+ className={triggerClassName}
675
+ data-status={status}
676
+ disabled={disabled}
677
+ onClick={handleClick}
678
+ type="button"
679
+ >
680
+ {content}
681
+ </button>
682
+ );
683
+ }
684
+
685
+ return (
686
+ <div
687
+ {...(props as HTMLAttributes<HTMLDivElement>)}
688
+ className={triggerClassName}
689
+ data-status={status}
690
+ >
691
+ {content}
692
+ </div>
693
+ );
694
+ };
695
+
696
+ export type ToolActivityContentProps = Omit<
697
+ HTMLAttributes<HTMLDivElement>,
698
+ "hidden" | "id"
699
+ >;
700
+
701
+ export const ToolActivityContent = ({
702
+ className,
703
+ ...props
704
+ }: ToolActivityContentProps) => {
705
+ const { expandable, isOpen, panelId } = useToolActivity();
706
+ if (!expandable || !isOpen) return null;
707
+ return (
708
+ <div
709
+ {...props}
710
+ className={classNames("cline-chat-tool-content", className)}
711
+ id={panelId}
712
+ />
713
+ );
714
+ };
715
+
716
+ export type ToolActivityDetailsProps = HTMLAttributes<HTMLDivElement>;
717
+
718
+ export const ToolActivityDetails = ({
719
+ className,
720
+ ...props
721
+ }: ToolActivityDetailsProps) => (
722
+ <div
723
+ className={classNames("cline-chat-tool-details", className)}
724
+ {...props}
725
+ />
726
+ );
727
+
728
+ export type ToolActivityCodeProps = HTMLAttributes<HTMLPreElement>;
729
+
730
+ export const ToolActivityCode = ({
731
+ className,
732
+ ...props
733
+ }: ToolActivityCodeProps) => (
734
+ <pre className={classNames("cline-chat-tool-code", className)} {...props} />
735
+ );
736
+
737
+ function ChevronDownIcon({ className }: { className?: string }) {
738
+ return (
739
+ <svg
740
+ aria-hidden="true"
741
+ className={className}
742
+ fill="none"
743
+ height="16"
744
+ viewBox="0 0 24 24"
745
+ width="16"
746
+ >
747
+ <path
748
+ d="m6 9 6 6 6-6"
749
+ stroke="currentColor"
750
+ strokeLinecap="round"
751
+ strokeLinejoin="round"
752
+ strokeWidth="2"
753
+ />
754
+ </svg>
755
+ );
756
+ }
757
+
758
+ function BrainIcon() {
759
+ return (
760
+ <svg
761
+ aria-hidden="true"
762
+ fill="none"
763
+ height="16"
764
+ viewBox="0 0 24 24"
765
+ width="16"
766
+ >
767
+ <path
768
+ d="M9.5 4.5A3 3 0 0 0 4 6a3 3 0 0 0 .5 5.9A3.5 3.5 0 0 0 8 17h1.5m5-12.5A3 3 0 0 1 20 6a3 3 0 0 1-.5 5.9A3.5 3.5 0 0 1 16 17h-1.5M9.5 4.5V20m5-15.5V20M9.5 9H7m7.5 3H17m-7.5 4H7m7.5 1h2"
769
+ stroke="currentColor"
770
+ strokeLinecap="round"
771
+ strokeLinejoin="round"
772
+ strokeWidth="1.75"
773
+ />
774
+ </svg>
775
+ );
776
+ }