@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.
- package/ADOPTION.md +497 -0
- package/README.md +172 -0
- package/components/agent-chat/agent-chat.css +383 -0
- package/components/agent-chat/index.tsx +776 -0
- package/dist/components/agent-chat/index.d.ts +68 -0
- package/dist/components/agent-chat/index.d.ts.map +1 -0
- package/dist/components/agent-chat/index.js +278 -0
- package/dist/components/agent-chat/index.js.map +1 -0
- package/package.json +88 -0
- package/theme/base.css +125 -0
- package/theme/index.css +3 -0
- package/theme/theme.css +57 -0
- package/theme/tokens.css +137 -0
|
@@ -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
|
+
}
|