@townco/ui 0.1.69 → 0.1.70
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/dist/core/schemas/tool-call.d.ts +8 -8
- package/dist/gui/components/Button.d.ts +1 -1
- package/dist/gui/components/ChatLayout.js +26 -141
- package/dist/gui/components/HookNotification.d.ts +9 -0
- package/dist/gui/components/HookNotification.js +50 -0
- package/dist/gui/components/InvokingGroup.d.ts +9 -0
- package/dist/gui/components/InvokingGroup.js +16 -0
- package/dist/gui/components/SubAgentDetails.js +13 -2
- package/dist/gui/components/SubagentStream.d.ts +23 -0
- package/dist/gui/components/SubagentStream.js +98 -0
- package/dist/gui/components/ToolCall.d.ts +8 -0
- package/dist/gui/components/ToolCall.js +234 -0
- package/dist/gui/components/ToolCallGroup.d.ts +8 -0
- package/dist/gui/components/ToolCallGroup.js +29 -0
- package/dist/gui/components/ToolOperation.js +43 -3
- package/dist/sdk/schemas/message.d.ts +2 -2
- package/dist/sdk/schemas/session.d.ts +6 -6
- package/package.json +3 -3
|
@@ -13,16 +13,16 @@ export type ToolCallStatus = z.infer<typeof ToolCallStatusSchema>;
|
|
|
13
13
|
* Tool call categories for UI presentation
|
|
14
14
|
*/
|
|
15
15
|
export declare const ToolCallKindSchema: z.ZodEnum<{
|
|
16
|
+
search: "search";
|
|
17
|
+
execute: "execute";
|
|
18
|
+
move: "move";
|
|
19
|
+
other: "other";
|
|
16
20
|
read: "read";
|
|
17
21
|
edit: "edit";
|
|
18
22
|
delete: "delete";
|
|
19
|
-
move: "move";
|
|
20
|
-
search: "search";
|
|
21
|
-
execute: "execute";
|
|
22
23
|
think: "think";
|
|
23
24
|
fetch: "fetch";
|
|
24
25
|
switch_mode: "switch_mode";
|
|
25
|
-
other: "other";
|
|
26
26
|
}>;
|
|
27
27
|
export type ToolCallKind = z.infer<typeof ToolCallKindSchema>;
|
|
28
28
|
/**
|
|
@@ -280,16 +280,16 @@ export declare const ToolCallSchema: z.ZodObject<{
|
|
|
280
280
|
}, z.core.$strip>>;
|
|
281
281
|
subline: z.ZodOptional<z.ZodString>;
|
|
282
282
|
kind: z.ZodEnum<{
|
|
283
|
+
search: "search";
|
|
284
|
+
execute: "execute";
|
|
285
|
+
move: "move";
|
|
286
|
+
other: "other";
|
|
283
287
|
read: "read";
|
|
284
288
|
edit: "edit";
|
|
285
289
|
delete: "delete";
|
|
286
|
-
move: "move";
|
|
287
|
-
search: "search";
|
|
288
|
-
execute: "execute";
|
|
289
290
|
think: "think";
|
|
290
291
|
fetch: "fetch";
|
|
291
292
|
switch_mode: "switch_mode";
|
|
292
|
-
other: "other";
|
|
293
293
|
}>;
|
|
294
294
|
status: z.ZodEnum<{
|
|
295
295
|
pending: "pending";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type VariantProps } from "class-variance-authority";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
declare const buttonVariants: (props?: ({
|
|
4
|
-
variant?: "
|
|
4
|
+
variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
|
|
5
5
|
size?: "default" | "icon" | "sm" | "lg" | null | undefined;
|
|
6
6
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
7
7
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
|
|
@@ -62,8 +62,6 @@ ChatLayoutBody.displayName = "ChatLayout.Body";
|
|
|
62
62
|
const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, initialScrollToBottom = true, ...props }, ref) => {
|
|
63
63
|
const [showScrollButton, setShowScrollButton] = React.useState(false);
|
|
64
64
|
const scrollContainerRef = React.useRef(null);
|
|
65
|
-
const wasAtBottomRef = React.useRef(true); // Track if user was at bottom before content update
|
|
66
|
-
const isAutoScrollingRef = React.useRef(false); // Track if we're programmatically scrolling
|
|
67
65
|
const hasInitialScrolledRef = React.useRef(false); // Track if initial scroll has happened
|
|
68
66
|
// Merge refs
|
|
69
67
|
React.useImperativeHandle(ref, () => scrollContainerRef.current);
|
|
@@ -79,170 +77,57 @@ const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChan
|
|
|
79
77
|
onScrollChange?.(isAtBottom);
|
|
80
78
|
return isAtBottom;
|
|
81
79
|
}, [onScrollChange, showScrollToBottom]);
|
|
82
|
-
// Handle scroll events
|
|
80
|
+
// Handle scroll events - update button visibility
|
|
83
81
|
const handleScroll = React.useCallback(() => {
|
|
84
|
-
|
|
85
|
-
if (isAutoScrollingRef.current) {
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
// This is a user-initiated scroll, update the position
|
|
89
|
-
const isAtBottom = checkScrollPosition();
|
|
90
|
-
wasAtBottomRef.current = isAtBottom;
|
|
82
|
+
checkScrollPosition();
|
|
91
83
|
}, [checkScrollPosition]);
|
|
92
|
-
// Scroll to bottom function
|
|
84
|
+
// Scroll to bottom function (for button click)
|
|
93
85
|
const scrollToBottom = React.useCallback((smooth = true) => {
|
|
94
86
|
const container = scrollContainerRef.current;
|
|
95
87
|
if (!container)
|
|
96
88
|
return;
|
|
97
|
-
// Mark that we're about to programmatically scroll
|
|
98
|
-
isAutoScrollingRef.current = true;
|
|
99
|
-
wasAtBottomRef.current = true; // Set immediately for instant scrolls
|
|
100
89
|
container.scrollTo({
|
|
101
90
|
top: container.scrollHeight,
|
|
102
91
|
behavior: smooth ? "smooth" : "auto",
|
|
103
92
|
});
|
|
104
|
-
// Clear the flag after scroll completes
|
|
105
|
-
// For instant scrolling, clear immediately; for smooth, wait
|
|
106
|
-
setTimeout(() => {
|
|
107
|
-
isAutoScrollingRef.current = false;
|
|
108
|
-
}, smooth ? 300 : 0);
|
|
109
93
|
}, []);
|
|
110
|
-
// Auto-scroll when content changes if user
|
|
94
|
+
// Auto-scroll when content changes ONLY if user is currently at the bottom
|
|
111
95
|
React.useEffect(() => {
|
|
112
96
|
const container = scrollContainerRef.current;
|
|
113
97
|
if (!container)
|
|
114
98
|
return;
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
99
|
+
// Check if user is CURRENTLY at the bottom (not just "was" at bottom)
|
|
100
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
101
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
102
|
+
const isCurrentlyAtBottom = distanceFromBottom < 100;
|
|
103
|
+
// Only auto-scroll if user is at the bottom right now
|
|
104
|
+
if (isCurrentlyAtBottom) {
|
|
118
105
|
requestAnimationFrame(() => {
|
|
119
|
-
|
|
106
|
+
container.scrollTop = container.scrollHeight;
|
|
120
107
|
});
|
|
121
108
|
}
|
|
122
|
-
// Update scroll
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}, [children, scrollToBottom, checkScrollPosition]);
|
|
127
|
-
// Track last scroll height to detect when content stops loading
|
|
128
|
-
const lastScrollHeightRef = React.useRef(0);
|
|
129
|
-
const scrollStableCountRef = React.useRef(0);
|
|
130
|
-
// Scroll to bottom on initial mount and during session loading
|
|
131
|
-
// Keep scrolling until content stabilizes (no more changes)
|
|
109
|
+
// Update the scroll button visibility
|
|
110
|
+
setShowScrollButton(!isCurrentlyAtBottom && showScrollToBottom);
|
|
111
|
+
}, [children, showScrollToBottom]);
|
|
112
|
+
// Scroll to bottom on initial mount only (for session replay)
|
|
132
113
|
React.useEffect(() => {
|
|
133
114
|
if (!initialScrollToBottom)
|
|
134
|
-
return;
|
|
115
|
+
return undefined;
|
|
135
116
|
const container = scrollContainerRef.current;
|
|
136
117
|
if (!container)
|
|
137
|
-
return;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
// Check if content has stabilized (scrollHeight hasn't changed)
|
|
143
|
-
const currentHeight = container.scrollHeight;
|
|
144
|
-
if (currentHeight === lastScrollHeightRef.current) {
|
|
145
|
-
scrollStableCountRef.current++;
|
|
146
|
-
}
|
|
147
|
-
else {
|
|
148
|
-
scrollStableCountRef.current = 0;
|
|
149
|
-
lastScrollHeightRef.current = currentHeight;
|
|
150
|
-
}
|
|
151
|
-
// If content is still loading (height changing) or we haven't scrolled yet,
|
|
152
|
-
// keep auto-scrolling. Stop after content is stable for a few renders.
|
|
153
|
-
if (scrollStableCountRef.current < 3) {
|
|
154
|
-
isAutoScrollingRef.current = true;
|
|
155
|
-
scrollToBottomInstant();
|
|
156
|
-
hasInitialScrolledRef.current = true;
|
|
157
|
-
}
|
|
158
|
-
else {
|
|
159
|
-
// Content is stable, stop auto-scrolling
|
|
160
|
-
isAutoScrollingRef.current = false;
|
|
161
|
-
}
|
|
162
|
-
}, [initialScrollToBottom, children]);
|
|
163
|
-
// Also use a timer-based approach as backup for session replay
|
|
164
|
-
// which may not trigger children changes
|
|
165
|
-
React.useEffect(() => {
|
|
166
|
-
if (!initialScrollToBottom)
|
|
167
|
-
return;
|
|
168
|
-
const container = scrollContainerRef.current;
|
|
169
|
-
if (!container)
|
|
170
|
-
return;
|
|
171
|
-
// Keep scrolling to bottom for the first 2 seconds of session load
|
|
172
|
-
// to catch async message replay
|
|
173
|
-
let cancelled = false;
|
|
174
|
-
const scrollInterval = setInterval(() => {
|
|
175
|
-
if (cancelled)
|
|
176
|
-
return;
|
|
177
|
-
if (container.scrollHeight > container.clientHeight) {
|
|
178
|
-
isAutoScrollingRef.current = true;
|
|
118
|
+
return undefined;
|
|
119
|
+
// Only scroll on initial mount, not on subsequent renders
|
|
120
|
+
if (!hasInitialScrolledRef.current) {
|
|
121
|
+
// Use a small delay to let initial content render
|
|
122
|
+
const timeout = setTimeout(() => {
|
|
179
123
|
container.scrollTop = container.scrollHeight;
|
|
180
|
-
wasAtBottomRef.current = true;
|
|
181
124
|
hasInitialScrolledRef.current = true;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Stop after 2 seconds
|
|
185
|
-
const timeout = setTimeout(() => {
|
|
186
|
-
clearInterval(scrollInterval);
|
|
187
|
-
isAutoScrollingRef.current = false;
|
|
188
|
-
}, 2000);
|
|
189
|
-
return () => {
|
|
190
|
-
cancelled = true;
|
|
191
|
-
clearInterval(scrollInterval);
|
|
192
|
-
clearTimeout(timeout);
|
|
193
|
-
};
|
|
194
|
-
}, [initialScrollToBottom]); // Only run once on mount
|
|
195
|
-
// Check scroll position on mount
|
|
196
|
-
React.useEffect(() => {
|
|
197
|
-
if (!isAutoScrollingRef.current) {
|
|
198
|
-
const isAtBottom = checkScrollPosition();
|
|
199
|
-
wasAtBottomRef.current = isAtBottom;
|
|
125
|
+
}, 100);
|
|
126
|
+
return () => clearTimeout(timeout);
|
|
200
127
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
// Immediately mark that user is interacting
|
|
205
|
-
isAutoScrollingRef.current = false;
|
|
206
|
-
// For wheel/touch events, temporarily break auto-scroll
|
|
207
|
-
// The actual scroll event will update wasAtBottomRef properly
|
|
208
|
-
// This prevents the race condition where content updates before scroll completes
|
|
209
|
-
const container = scrollContainerRef.current;
|
|
210
|
-
if (!container)
|
|
211
|
-
return;
|
|
212
|
-
// Check current position BEFORE the scroll happens
|
|
213
|
-
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
214
|
-
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
215
|
-
// If user is not currently at the bottom, definitely break auto-scroll
|
|
216
|
-
if (distanceFromBottom >= 100) {
|
|
217
|
-
wasAtBottomRef.current = false;
|
|
218
|
-
}
|
|
219
|
-
// If they are at bottom, the scroll event will determine if they stay there
|
|
220
|
-
}, []);
|
|
221
|
-
// Handle keyboard navigation
|
|
222
|
-
const handleKeyDown = React.useCallback((e) => {
|
|
223
|
-
// If user presses arrow keys, page up/down, home/end - they're scrolling
|
|
224
|
-
const scrollKeys = [
|
|
225
|
-
"ArrowUp",
|
|
226
|
-
"ArrowDown",
|
|
227
|
-
"PageUp",
|
|
228
|
-
"PageDown",
|
|
229
|
-
"Home",
|
|
230
|
-
"End",
|
|
231
|
-
];
|
|
232
|
-
if (scrollKeys.includes(e.key)) {
|
|
233
|
-
isAutoScrollingRef.current = false;
|
|
234
|
-
// Check position on next frame after the scroll happens
|
|
235
|
-
requestAnimationFrame(() => {
|
|
236
|
-
const container = scrollContainerRef.current;
|
|
237
|
-
if (!container)
|
|
238
|
-
return;
|
|
239
|
-
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
240
|
-
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
241
|
-
wasAtBottomRef.current = distanceFromBottom < 100;
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}, []);
|
|
245
|
-
return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), onScroll: handleScroll, onWheel: handleUserInteraction, onTouchStart: handleUserInteraction, onKeyDown: handleKeyDown, tabIndex: 0, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollButton && (_jsx("button", { type: "button", onClick: () => scrollToBottom(true), className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", "animate-in fade-in slide-in-from-bottom-2"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
|
|
128
|
+
return undefined;
|
|
129
|
+
}, [initialScrollToBottom]);
|
|
130
|
+
return (_jsxs("div", { className: "relative flex-1 overflow-hidden", children: [_jsx("div", { ref: scrollContainerRef, className: cn("h-full overflow-y-auto flex flex-col", className), onScroll: handleScroll, ...props, children: _jsx("div", { className: "mx-auto max-w-chat flex-1 w-full flex flex-col", children: children }) }), showScrollButton && (_jsx("button", { type: "button", onClick: () => scrollToBottom(true), className: cn("absolute bottom-4 left-1/2 -translate-x-1/2 z-10", "flex items-center justify-center p-2 rounded-full", "bg-card border border-border shadow-lg", "text-foreground", "hover:bg-accent hover:text-accent-foreground", "transition-all duration-200 ease-in-out", "animate-in fade-in slide-in-from-bottom-2"), "aria-label": "Scroll to bottom", children: _jsx(ArrowDown, { className: "size-4" }) }))] }));
|
|
246
131
|
});
|
|
247
132
|
ChatLayoutMessages.displayName = "ChatLayout.Messages";
|
|
248
133
|
const ChatLayoutFooter = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { HookNotificationDisplay } from "../../core/schemas/chat.js";
|
|
2
|
+
export interface HookNotificationProps {
|
|
3
|
+
notification: HookNotificationDisplay;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* HookNotification component - displays a hook notification inline with messages
|
|
7
|
+
* Only shows completed or error states (not intermediate "triggered" state)
|
|
8
|
+
*/
|
|
9
|
+
export declare function HookNotification({ notification }: HookNotificationProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { AlertCircle, Archive, CheckCircle2, ChevronDown, Scissors, } from "lucide-react";
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
/**
|
|
5
|
+
* Get display information for a hook type
|
|
6
|
+
*/
|
|
7
|
+
function getHookDisplayInfo(hookType, _callback) {
|
|
8
|
+
if (hookType === "context_size") {
|
|
9
|
+
return {
|
|
10
|
+
icon: Archive,
|
|
11
|
+
title: "Context Compacted",
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (hookType === "tool_response") {
|
|
15
|
+
return {
|
|
16
|
+
icon: Scissors,
|
|
17
|
+
title: "Tool Response Compacted",
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
// Fallback for unknown hook types
|
|
21
|
+
return {
|
|
22
|
+
icon: Archive,
|
|
23
|
+
title: `Hook Executed`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Format a number with thousand separators
|
|
28
|
+
*/
|
|
29
|
+
function formatNumber(num) {
|
|
30
|
+
return num.toLocaleString();
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* HookNotification component - displays a hook notification inline with messages
|
|
34
|
+
* Only shows completed or error states (not intermediate "triggered" state)
|
|
35
|
+
*/
|
|
36
|
+
export function HookNotification({ notification }) {
|
|
37
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
38
|
+
const { icon: IconComponent, title } = getHookDisplayInfo(notification.hookType, notification.callback);
|
|
39
|
+
const isCompleted = notification.status === "completed";
|
|
40
|
+
const isError = notification.status === "error";
|
|
41
|
+
// Build subtitle showing key info
|
|
42
|
+
let subtitle = "";
|
|
43
|
+
if (isCompleted && notification.metadata?.tokensSaved !== undefined) {
|
|
44
|
+
subtitle = `${formatNumber(notification.metadata.tokensSaved)} tokens saved`;
|
|
45
|
+
}
|
|
46
|
+
else if (isError && notification.error) {
|
|
47
|
+
subtitle = notification.error;
|
|
48
|
+
}
|
|
49
|
+
return (_jsxs("div", { className: "flex flex-col my-3", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: isError ? "text-destructive" : "text-muted-foreground", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: title }), isCompleted && _jsx(CheckCircle2, { className: "h-3 w-3 text-green-500" }), isError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` })] }), subtitle && (_jsx("span", { className: `text-paragraph-sm pl-4.5 ${isError ? "text-destructive/70" : "text-muted-foreground/70"}`, children: subtitle }))] }), isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Hook Details" }), _jsxs("div", { className: "space-y-1 text-[11px]", children: [_jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Type:" }), _jsx("span", { className: "text-foreground font-mono", children: notification.hookType })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Callback:" }), _jsx("span", { className: "text-foreground font-mono", children: notification.callback })] })] })] }), notification.metadata && (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Result" }), _jsxs("div", { className: "space-y-1 text-[11px]", children: [notification.metadata.action && (_jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Action:" }), _jsx("span", { className: "text-foreground", children: notification.metadata.action })] })), notification.metadata.messagesRemoved !== undefined && (_jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Messages Removed:" }), _jsx("span", { className: "text-foreground", children: formatNumber(notification.metadata.messagesRemoved) })] })), notification.metadata.tokensSaved !== undefined && (_jsxs("div", { className: "flex gap-2", children: [_jsx("span", { className: "text-muted-foreground", children: "Tokens Saved:" }), _jsx("span", { className: "text-green-500 font-medium", children: formatNumber(notification.metadata.tokensSaved) })] }))] })] })), notification.error && (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [_jsx("div", { className: "text-[10px] font-bold text-destructive uppercase tracking-wider mb-1.5 font-sans", children: "Error" }), _jsx("div", { className: "text-[11px] text-destructive font-mono", children: notification.error })] })), notification.completedAt && (_jsxs("div", { className: "p-2 bg-muted/50 border-t border-border text-[10px] text-muted-foreground font-sans", children: ["Executed:", " ", new Date(notification.completedAt).toLocaleTimeString()] }))] }))] }));
|
|
50
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
|
|
2
|
+
export interface InvokingGroupProps {
|
|
3
|
+
toolCalls: ToolCallType[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* InvokingGroup component - displays a group of preliminary (invoking) tool calls
|
|
7
|
+
* Shows as "Invoking parallel operation (N)" with a summary of unique tool names
|
|
8
|
+
*/
|
|
9
|
+
export declare function InvokingGroup({ toolCalls }: InvokingGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ListVideo } from "lucide-react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
/**
|
|
5
|
+
* InvokingGroup component - displays a group of preliminary (invoking) tool calls
|
|
6
|
+
* Shows as "Invoking parallel operation (N)" with a summary of unique tool names
|
|
7
|
+
*/
|
|
8
|
+
export function InvokingGroup({ toolCalls }) {
|
|
9
|
+
// Get unique display names for the summary
|
|
10
|
+
const displayNames = toolCalls.map((tc) => tc.prettyName || tc.title);
|
|
11
|
+
const uniqueNames = [...new Set(displayNames)];
|
|
12
|
+
const summary = uniqueNames.length <= 2
|
|
13
|
+
? uniqueNames.join(", ")
|
|
14
|
+
: `${uniqueNames.slice(0, 2).join(", ")} +${uniqueNames.length - 2} more`;
|
|
15
|
+
return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-paragraph-sm text-muted-foreground/50", children: [_jsx(ListVideo, { className: "h-3 w-3" }), _jsx("span", { children: "Invoking parallel operation" }), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-muted-foreground/50", children: toolCalls.length })] }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground/50 pl-4.5", children: summary })] }));
|
|
16
|
+
}
|
|
@@ -19,7 +19,8 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
|
|
|
19
19
|
// Use controlled state if provided, otherwise use internal state
|
|
20
20
|
const isExpanded = controlledIsExpanded ?? internalIsExpanded;
|
|
21
21
|
const setIsExpanded = onExpandChange ?? setInternalIsExpanded;
|
|
22
|
-
|
|
22
|
+
// Start with Thinking expanded while running to show the live stream
|
|
23
|
+
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
|
23
24
|
const [isNearBottom, setIsNearBottom] = useState(true);
|
|
24
25
|
const thinkingContainerRef = useRef(null);
|
|
25
26
|
// Only use SSE streaming if not in replay mode and port/sessionId provided
|
|
@@ -99,6 +100,16 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
|
|
|
99
100
|
checkScrollPosition(); // Check initial position
|
|
100
101
|
return () => container.removeEventListener("scroll", handleScroll);
|
|
101
102
|
}, [checkScrollPosition, isThinkingExpanded, isExpanded]);
|
|
103
|
+
// When thinking section expands, scroll to bottom and reset follow state
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (isThinkingExpanded) {
|
|
106
|
+
setIsNearBottom(true);
|
|
107
|
+
// Use requestAnimationFrame to ensure DOM has rendered
|
|
108
|
+
requestAnimationFrame(() => {
|
|
109
|
+
scrollToBottom();
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}, [isThinkingExpanded, scrollToBottom]);
|
|
102
113
|
// Get last line of streaming content for preview
|
|
103
114
|
const lastLine = currentMessage?.content
|
|
104
115
|
? currentMessage.content.split("\n").filter(Boolean).pop() || ""
|
|
@@ -109,7 +120,7 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
|
|
|
109
120
|
? (query.split("\n")[0] ?? "").slice(0, 100) +
|
|
110
121
|
(query.length > 100 ? "..." : "")
|
|
111
122
|
: "";
|
|
112
|
-
return (_jsxs("div", { children: [!isExpanded && (_jsx("div", { className: "w-full max-w-md", children: previewText ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/70 truncate", children: previewText })) : queryFirstLine ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 truncate", children: queryFirstLine })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsxs("div", { className: "text-[11px] font-mono space-y-1", children: [agentName && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "agentName: " }), _jsx("span", { className: "text-foreground", children: agentName })] })), query && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "query: " }), _jsx("span", { className: "text-foreground", children: query })] }))] })] })), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setIsThinkingExpanded(!isThinkingExpanded), className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider font-sans", children: "
|
|
123
|
+
return (_jsxs("div", { children: [!isExpanded && (_jsx("div", { className: "w-full max-w-md", children: previewText ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/70 truncate", children: previewText })) : queryFirstLine ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 truncate", children: queryFirstLine })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsxs("div", { className: "text-[11px] font-mono space-y-1", children: [agentName && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "agentName: " }), _jsx("span", { className: "text-foreground", children: agentName })] })), query && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "query: " }), _jsx("span", { className: "text-foreground", children: query })] }))] })] })), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setIsThinkingExpanded(!isThinkingExpanded), className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider font-sans", children: "Stream" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isThinkingExpanded ? "rotate-180" : ""}` })] }), isThinkingExpanded && (_jsxs("div", { ref: thinkingContainerRef, className: "mt-2 rounded-md overflow-hidden bg-muted/30 border border-border/50 max-h-[200px] overflow-y-auto", children: [error && (_jsxs("div", { className: "px-2 py-2 text-[11px] text-destructive", children: ["Error: ", error] })), !error && !hasContent && isRunning && (_jsx("div", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: "Waiting for sub-agent response..." })), currentMessage && (_jsxs("div", { className: "px-2 py-2 space-y-2", children: [currentMessage.contentBlocks &&
|
|
113
124
|
currentMessage.contentBlocks.length > 0
|
|
114
125
|
? // Render interleaved content blocks
|
|
115
126
|
currentMessage.contentBlocks.map((block, idx) => block.type === "text" ? (_jsx("div", { className: "text-[11px] text-foreground prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-pre:my-1 prose-code:text-[10px]", children: _jsx(MarkdownRenderer, { content: block.text }) }, `text-${idx}`)) : (_jsx(SubagentToolCallItem, { toolCall: block.toolCall }, block.toolCall.id)))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface SubagentStreamProps {
|
|
2
|
+
/** Sub-agent HTTP port */
|
|
3
|
+
port: number;
|
|
4
|
+
/** Sub-agent session ID */
|
|
5
|
+
sessionId: string;
|
|
6
|
+
/** Optional host (defaults to localhost) */
|
|
7
|
+
host?: string;
|
|
8
|
+
/** Parent tool call status - use this to determine if sub-agent is running */
|
|
9
|
+
parentStatus?: "pending" | "in_progress" | "completed" | "failed";
|
|
10
|
+
/** Sub-agent name (for display) */
|
|
11
|
+
agentName?: string | undefined;
|
|
12
|
+
/** Query sent to the sub-agent */
|
|
13
|
+
query?: string | undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* SubagentStream component - displays streaming content from a sub-agent.
|
|
17
|
+
*
|
|
18
|
+
* This component:
|
|
19
|
+
* - Connects directly to the sub-agent's SSE endpoint
|
|
20
|
+
* - Displays streaming text and tool calls
|
|
21
|
+
* - Renders in a collapsible section (collapsed by default)
|
|
22
|
+
*/
|
|
23
|
+
export declare function SubagentStream({ port, sessionId, host, parentStatus, agentName, query, }: SubagentStreamProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, CircleDot, Loader2 } from "lucide-react";
|
|
3
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { useSubagentStream } from "../../core/hooks/use-subagent-stream.js";
|
|
5
|
+
const SCROLL_THRESHOLD = 50; // px from bottom to consider "at bottom"
|
|
6
|
+
/**
|
|
7
|
+
* SubagentStream component - displays streaming content from a sub-agent.
|
|
8
|
+
*
|
|
9
|
+
* This component:
|
|
10
|
+
* - Connects directly to the sub-agent's SSE endpoint
|
|
11
|
+
* - Displays streaming text and tool calls
|
|
12
|
+
* - Renders in a collapsible section (collapsed by default)
|
|
13
|
+
*/
|
|
14
|
+
export function SubagentStream({ port, sessionId, host, parentStatus, agentName, query, }) {
|
|
15
|
+
const [isExpanded, setIsExpanded] = useState(false); // Start collapsed for parallel ops
|
|
16
|
+
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
|
17
|
+
const [isNearBottom, setIsNearBottom] = useState(true);
|
|
18
|
+
const thinkingContainerRef = useRef(null);
|
|
19
|
+
const { messages, isStreaming: hookIsStreaming, error } = useSubagentStream({
|
|
20
|
+
port,
|
|
21
|
+
sessionId,
|
|
22
|
+
...(host !== undefined ? { host } : {}),
|
|
23
|
+
});
|
|
24
|
+
// Use parent status as primary indicator, fall back to hook's streaming state
|
|
25
|
+
// Parent is "in_progress" means sub-agent is definitely still running
|
|
26
|
+
const isRunning = parentStatus === "in_progress" || parentStatus === "pending" || hookIsStreaming;
|
|
27
|
+
// Get the current/latest message
|
|
28
|
+
const currentMessage = messages[messages.length - 1];
|
|
29
|
+
const hasContent = currentMessage &&
|
|
30
|
+
(currentMessage.content ||
|
|
31
|
+
(currentMessage.toolCalls && currentMessage.toolCalls.length > 0));
|
|
32
|
+
// Auto-collapse Thinking when completed (so Output is the primary view)
|
|
33
|
+
const prevIsRunningRef = useRef(isRunning);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (prevIsRunningRef.current && !isRunning) {
|
|
36
|
+
// Just completed - collapse thinking to show output
|
|
37
|
+
setIsThinkingExpanded(false);
|
|
38
|
+
}
|
|
39
|
+
prevIsRunningRef.current = isRunning;
|
|
40
|
+
}, [isRunning]);
|
|
41
|
+
// Check if user is near bottom of scroll area
|
|
42
|
+
const checkScrollPosition = useCallback(() => {
|
|
43
|
+
const container = thinkingContainerRef.current;
|
|
44
|
+
if (!container)
|
|
45
|
+
return;
|
|
46
|
+
const { scrollTop, scrollHeight, clientHeight } = container;
|
|
47
|
+
const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
|
|
48
|
+
setIsNearBottom(distanceFromBottom < SCROLL_THRESHOLD);
|
|
49
|
+
}, []);
|
|
50
|
+
// Scroll to bottom
|
|
51
|
+
const scrollToBottom = useCallback(() => {
|
|
52
|
+
const container = thinkingContainerRef.current;
|
|
53
|
+
if (!container)
|
|
54
|
+
return;
|
|
55
|
+
container.scrollTop = container.scrollHeight;
|
|
56
|
+
}, []);
|
|
57
|
+
// Auto-scroll when content changes and user is near bottom
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
if (isNearBottom && (isRunning || hasContent)) {
|
|
60
|
+
scrollToBottom();
|
|
61
|
+
}
|
|
62
|
+
}, [currentMessage?.content, currentMessage?.toolCalls, isNearBottom, isRunning, hasContent, scrollToBottom]);
|
|
63
|
+
// Set up scroll listener
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
const container = thinkingContainerRef.current;
|
|
66
|
+
if (!container)
|
|
67
|
+
return;
|
|
68
|
+
const handleScroll = () => checkScrollPosition();
|
|
69
|
+
container.addEventListener("scroll", handleScroll, { passive: true });
|
|
70
|
+
checkScrollPosition(); // Check initial position
|
|
71
|
+
return () => container.removeEventListener("scroll", handleScroll);
|
|
72
|
+
}, [checkScrollPosition, isThinkingExpanded, isExpanded]);
|
|
73
|
+
// Get last line of streaming content for preview
|
|
74
|
+
const lastLine = currentMessage?.content
|
|
75
|
+
? currentMessage.content.split("\n").filter(Boolean).pop() || ""
|
|
76
|
+
: "";
|
|
77
|
+
const previewText = lastLine.length > 100 ? `${lastLine.slice(0, 100)}...` : lastLine;
|
|
78
|
+
return (_jsxs("div", { children: [!isExpanded && (_jsx("button", { type: "button", onClick: () => setIsExpanded(true), className: "w-full max-w-md text-left cursor-pointer bg-transparent border-none p-0", children: previewText ? (_jsx("p", { className: `text-paragraph-sm text-muted-foreground truncate ${isRunning ? "animate-pulse" : ""}`, children: previewText })) : isRunning ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 italic animate-pulse", children: "Waiting for response..." })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsxs("div", { className: "text-[11px] font-mono space-y-1", children: [agentName && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "agentName: " }), _jsx("span", { className: "text-foreground", children: agentName })] })), query && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "query: " }), _jsx("span", { className: "text-foreground", children: query })] }))] })] })), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setIsThinkingExpanded(!isThinkingExpanded), className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider font-sans", children: "Thinking" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isThinkingExpanded ? "rotate-180" : ""}` })] }), isThinkingExpanded && (_jsxs("div", { ref: thinkingContainerRef, className: "mt-2 rounded-md overflow-hidden bg-muted/30 border border-border/50 max-h-[200px] overflow-y-auto", children: [error && (_jsxs("div", { className: "px-2 py-2 text-[11px] text-destructive", children: ["Error: ", error] })), !error && !hasContent && isRunning && (_jsx("div", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: "Waiting for sub-agent response..." })), currentMessage && (_jsxs("div", { className: "px-2 py-2 space-y-2", children: [currentMessage.toolCalls &&
|
|
79
|
+
currentMessage.toolCalls.length > 0 && (_jsx("div", { className: "space-y-1", children: currentMessage.toolCalls.map((tc) => (_jsx(SubagentToolCallItem, { toolCall: tc }, tc.id))) })), currentMessage.content && (_jsxs("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono", children: [currentMessage.content, currentMessage.isStreaming && (_jsx("span", { className: "inline-block w-1.5 h-3 bg-primary/70 ml-0.5 animate-pulse" }))] }))] }))] }))] }), !isRunning && currentMessage?.content && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono max-h-[200px] overflow-y-auto rounded-md bg-muted/30 border border-border/50 px-2 py-2", children: currentMessage.content })] }))] }))] }));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Simple tool call display for sub-agent tool calls
|
|
83
|
+
*/
|
|
84
|
+
function SubagentToolCallItem({ toolCall }) {
|
|
85
|
+
const statusIcon = {
|
|
86
|
+
pending: "...",
|
|
87
|
+
in_progress: "",
|
|
88
|
+
completed: "",
|
|
89
|
+
failed: "",
|
|
90
|
+
}[toolCall.status];
|
|
91
|
+
const statusColor = {
|
|
92
|
+
pending: "text-muted-foreground",
|
|
93
|
+
in_progress: "text-blue-500",
|
|
94
|
+
completed: "text-green-500",
|
|
95
|
+
failed: "text-destructive",
|
|
96
|
+
}[toolCall.status];
|
|
97
|
+
return (_jsxs("div", { className: "flex items-center gap-2 text-[10px] text-muted-foreground", children: [_jsx("span", { className: statusColor, children: statusIcon }), _jsx("span", { className: "font-medium", children: toolCall.prettyName || toolCall.title }), toolCall.status === "in_progress" && (_jsx(Loader2, { className: "h-2.5 w-2.5 animate-spin" }))] }));
|
|
98
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
|
|
2
|
+
export interface ToolCallProps {
|
|
3
|
+
toolCall: ToolCallType;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* ToolCall component - displays a single tool call with collapsible details
|
|
7
|
+
*/
|
|
8
|
+
export declare function ToolCall({ toolCall }: ToolCallProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import JsonView from "@uiw/react-json-view";
|
|
3
|
+
import { AlertCircle, CheckSquare, ChevronDown, ChevronRight, CircleDot, Cloud, Edit, FileText, Globe, Image, Link, Search, Wrench, } from "lucide-react";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import { ChatLayout } from "./index.js";
|
|
6
|
+
import { SubAgentDetails } from "./SubAgentDetails.js";
|
|
7
|
+
import { useTheme } from "./ThemeProvider.js";
|
|
8
|
+
/**
|
|
9
|
+
* Map of icon names to Lucide components
|
|
10
|
+
*/
|
|
11
|
+
const ICON_MAP = {
|
|
12
|
+
Globe: Globe,
|
|
13
|
+
Image: Image,
|
|
14
|
+
Link: Link,
|
|
15
|
+
Cloud: Cloud,
|
|
16
|
+
CheckSquare: CheckSquare,
|
|
17
|
+
Search: Search,
|
|
18
|
+
FileText: FileText,
|
|
19
|
+
Edit: Edit,
|
|
20
|
+
Wrench: Wrench,
|
|
21
|
+
CircleDot: CircleDot,
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Tool call kind icons (using emoji for simplicity)
|
|
25
|
+
*/
|
|
26
|
+
const _kindIcons = {
|
|
27
|
+
read: "\u{1F4C4}",
|
|
28
|
+
edit: "\u{270F}\u{FE0F}",
|
|
29
|
+
delete: "\u{1F5D1}\u{FE0F}",
|
|
30
|
+
move: "\u{1F4E6}",
|
|
31
|
+
search: "\u{1F50D}",
|
|
32
|
+
execute: "\u{2699}\u{FE0F}",
|
|
33
|
+
think: "\u{1F4AD}",
|
|
34
|
+
fetch: "\u{1F310}",
|
|
35
|
+
switch_mode: "\u{1F501}",
|
|
36
|
+
other: "\u{1F527}",
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* ToolCall component - displays a single tool call with collapsible details
|
|
40
|
+
*/
|
|
41
|
+
export function ToolCall({ toolCall }) {
|
|
42
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
43
|
+
const [isSubagentExpanded, setIsSubagentExpanded] = useState(false);
|
|
44
|
+
const { resolvedTheme } = useTheme();
|
|
45
|
+
// Detect TodoWrite tool and subagent
|
|
46
|
+
const isTodoWrite = toolCall.title === "todo_write";
|
|
47
|
+
// A subagent call can be detected by:
|
|
48
|
+
// - Live: has port and sessionId (but no stored messages yet)
|
|
49
|
+
// - Replay: has stored subagentMessages
|
|
50
|
+
const hasLiveSubagent = !!(toolCall.subagentPort && toolCall.subagentSessionId);
|
|
51
|
+
const hasStoredSubagent = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
|
|
52
|
+
const isSubagentCall = hasLiveSubagent || hasStoredSubagent;
|
|
53
|
+
// Use replay mode if we have stored messages - they should take precedence
|
|
54
|
+
// over trying to connect to SSE (which won't work for replayed sessions)
|
|
55
|
+
const isReplaySubagent = hasStoredSubagent;
|
|
56
|
+
// Safely access ChatLayout context - will be undefined if not within ChatLayout
|
|
57
|
+
const layoutContext = React.useContext(ChatLayout.Context);
|
|
58
|
+
// Click handler: toggle sidepanel for TodoWrite, subagent details for subagents, expand for others
|
|
59
|
+
const handleHeaderClick = React.useCallback(() => {
|
|
60
|
+
if (isTodoWrite && layoutContext) {
|
|
61
|
+
// Toggle sidepanel - close if already open on todo tab, otherwise open
|
|
62
|
+
if (layoutContext.panelSize !== "hidden" &&
|
|
63
|
+
layoutContext.activeTab === "todo") {
|
|
64
|
+
layoutContext.setPanelSize("hidden");
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
layoutContext.setPanelSize("small");
|
|
68
|
+
layoutContext.setActiveTab("todo");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (isSubagentCall) {
|
|
72
|
+
// Toggle subagent details
|
|
73
|
+
setIsSubagentExpanded(!isSubagentExpanded);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Normal expand/collapse
|
|
77
|
+
setIsExpanded(!isExpanded);
|
|
78
|
+
}
|
|
79
|
+
}, [
|
|
80
|
+
isTodoWrite,
|
|
81
|
+
layoutContext,
|
|
82
|
+
isExpanded,
|
|
83
|
+
isSubagentCall,
|
|
84
|
+
isSubagentExpanded,
|
|
85
|
+
]);
|
|
86
|
+
// Determine which icon to show
|
|
87
|
+
const IconComponent = toolCall.icon && ICON_MAP[toolCall.icon]
|
|
88
|
+
? ICON_MAP[toolCall.icon]
|
|
89
|
+
: CircleDot;
|
|
90
|
+
// Determine display name
|
|
91
|
+
const displayName = toolCall.prettyName || toolCall.title;
|
|
92
|
+
// Determine icon color based on status (especially for subagents)
|
|
93
|
+
const isSubagentRunning = isSubagentCall &&
|
|
94
|
+
(toolCall.status === "in_progress" || toolCall.status === "pending");
|
|
95
|
+
const isSubagentFailed = isSubagentCall && toolCall.status === "failed";
|
|
96
|
+
const iconColorClass = isSubagentCall
|
|
97
|
+
? isSubagentFailed
|
|
98
|
+
? "text-destructive"
|
|
99
|
+
: isSubagentRunning
|
|
100
|
+
? "text-foreground animate-pulse"
|
|
101
|
+
: "text-green-500"
|
|
102
|
+
: "text-muted-foreground";
|
|
103
|
+
const statusTooltip = isSubagentCall
|
|
104
|
+
? isSubagentFailed
|
|
105
|
+
? "Sub-agent failed"
|
|
106
|
+
: isSubagentRunning
|
|
107
|
+
? "Sub-agent running"
|
|
108
|
+
: "Sub-agent completed"
|
|
109
|
+
: undefined;
|
|
110
|
+
// Check if there's an error
|
|
111
|
+
const hasError = toolCall.status === "failed" || !!toolCall.error;
|
|
112
|
+
// Check if this is a preliminary (pending) tool call without full details yet
|
|
113
|
+
const isPreliminary = toolCall.status === "pending" &&
|
|
114
|
+
(!toolCall.rawInput || Object.keys(toolCall.rawInput).length === 0);
|
|
115
|
+
// JSON View style based on theme
|
|
116
|
+
const jsonStyle = {
|
|
117
|
+
fontSize: "11px",
|
|
118
|
+
backgroundColor: "transparent",
|
|
119
|
+
fontFamily: "inherit",
|
|
120
|
+
"--w-rjv-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
|
|
121
|
+
"--w-rjv-key-string": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
|
|
122
|
+
"--w-rjv-background-color": "transparent",
|
|
123
|
+
"--w-rjv-line-color": resolvedTheme === "dark" ? "#27272a" : "#e4e4e7",
|
|
124
|
+
"--w-rjv-arrow-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
125
|
+
"--w-rjv-edit-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
|
|
126
|
+
"--w-rjv-info-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
127
|
+
"--w-rjv-update-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
|
|
128
|
+
"--w-rjv-copied-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
|
|
129
|
+
"--w-rjv-copied-success-color": resolvedTheme === "dark" ? "#22c55e" : "#16a34a",
|
|
130
|
+
"--w-rjv-curlybraces-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
131
|
+
"--w-rjv-colon-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
132
|
+
"--w-rjv-brackets-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
133
|
+
"--w-rjv-quotes-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
134
|
+
"--w-rjv-quotes-string-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
|
|
135
|
+
"--w-rjv-type-string-color": resolvedTheme === "dark" ? "#22c55e" : "#16a34a",
|
|
136
|
+
"--w-rjv-type-int-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
|
|
137
|
+
"--w-rjv-type-float-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
|
|
138
|
+
"--w-rjv-type-bigint-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
|
|
139
|
+
"--w-rjv-type-boolean-color": resolvedTheme === "dark" ? "#3b82f6" : "#2563eb",
|
|
140
|
+
"--w-rjv-type-date-color": resolvedTheme === "dark" ? "#ec4899" : "#db2777",
|
|
141
|
+
"--w-rjv-type-url-color": resolvedTheme === "dark" ? "#3b82f6" : "#2563eb",
|
|
142
|
+
"--w-rjv-type-null-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
|
|
143
|
+
"--w-rjv-type-nan-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
|
|
144
|
+
"--w-rjv-type-undefined-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
|
|
145
|
+
};
|
|
146
|
+
// Preliminary tool calls show as simple light gray text without expansion
|
|
147
|
+
if (isPreliminary) {
|
|
148
|
+
return (_jsx("div", { className: "flex flex-col my-4", children: _jsxs("span", { className: "text-paragraph-sm text-muted-foreground/50", children: ["Invoking ", displayName] }) }));
|
|
149
|
+
}
|
|
150
|
+
return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: iconColorClass, title: statusTooltip, children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: displayName }), hasError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground/70" })) : (_jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` }))] }), toolCall.subline && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: toolCall.subline }))] }), !isTodoWrite && isSubagentCall && (_jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, { port: toolCall.subagentPort, sessionId: toolCall.subagentSessionId, parentStatus: toolCall.status, agentName: toolCall.rawInput?.agentName, query: toolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded, storedMessages: toolCall.subagentMessages, isReplay: isReplaySubagent }) })), !isTodoWrite && !isSubagentCall && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.rawInput &&
|
|
151
|
+
Object.keys(toolCall.rawInput).length > 0 &&
|
|
152
|
+
!toolCall.subagentPort && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsx("div", { className: "text-[11px] font-mono text-foreground", children: _jsx(JsonView, { value: toolCall.rawInput, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) })] })), toolCall.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Files" }), _jsx("ul", { className: "space-y-1", children: toolCall.locations.map((loc) => (_jsxs("li", { className: "font-mono text-[11px] text-foreground bg-muted px-1.5 py-0.5 rounded w-fit", children: [loc.path, loc.line !== null &&
|
|
153
|
+
loc.line !== undefined &&
|
|
154
|
+
`:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), (toolCall.content && toolCall.content.length > 0) ||
|
|
155
|
+
toolCall.error ? (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsxs("div", { className: "space-y-2 text-[11px] text-foreground", children: [toolCall.content?.map((block, idx) => {
|
|
156
|
+
// Generate a stable key based on content
|
|
157
|
+
const getBlockKey = () => {
|
|
158
|
+
if (block.type === "diff" && "path" in block) {
|
|
159
|
+
return `diff-${block.path}-${idx}`;
|
|
160
|
+
}
|
|
161
|
+
if (block.type === "terminal" && "terminalId" in block) {
|
|
162
|
+
return `terminal-${block.terminalId}`;
|
|
163
|
+
}
|
|
164
|
+
if (block.type === "text" && "text" in block) {
|
|
165
|
+
return `text-${block.text.substring(0, 20)}-${idx}`;
|
|
166
|
+
}
|
|
167
|
+
if (block.type === "content" && "content" in block) {
|
|
168
|
+
const innerContent = block.content;
|
|
169
|
+
return `content-${innerContent.text?.substring(0, 20)}-${idx}`;
|
|
170
|
+
}
|
|
171
|
+
return `block-${idx}`;
|
|
172
|
+
};
|
|
173
|
+
// Helper to render text content (with JSON parsing if applicable)
|
|
174
|
+
const renderTextContent = (text, key) => {
|
|
175
|
+
// Try to parse as JSON
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(text);
|
|
178
|
+
// If it's an object or array, render with JsonView
|
|
179
|
+
if (typeof parsed === "object" && parsed !== null) {
|
|
180
|
+
return (_jsx("div", { className: "text-[11px]", children: _jsx(JsonView, { value: parsed, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) }, key));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Not valid JSON, render as plain text
|
|
185
|
+
}
|
|
186
|
+
// Render as plain text
|
|
187
|
+
return (_jsx("pre", { className: "whitespace-pre-wrap font-mono text-[11px] text-foreground overflow-x-auto", children: text }, key));
|
|
188
|
+
};
|
|
189
|
+
// Handle nested content blocks (ACP format)
|
|
190
|
+
if (block.type === "content" && "content" in block) {
|
|
191
|
+
const innerContent = block.content;
|
|
192
|
+
if (innerContent.type === "text" && innerContent.text) {
|
|
193
|
+
return renderTextContent(innerContent.text, getBlockKey());
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Handle direct text blocks
|
|
197
|
+
if (block.type === "text" && "text" in block) {
|
|
198
|
+
return renderTextContent(block.text, getBlockKey());
|
|
199
|
+
}
|
|
200
|
+
// Handle image blocks
|
|
201
|
+
if (block.type === "image") {
|
|
202
|
+
const alt = block.alt || "Generated image";
|
|
203
|
+
let imageSrc;
|
|
204
|
+
if ("data" in block) {
|
|
205
|
+
// Base64 encoded image
|
|
206
|
+
const mimeType = block.mimeType || "image/png";
|
|
207
|
+
imageSrc = `data:${mimeType};base64,${block.data}`;
|
|
208
|
+
}
|
|
209
|
+
else if ("url" in block) {
|
|
210
|
+
// URL or file path
|
|
211
|
+
imageSrc = block.url;
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return (_jsx("div", { className: "my-2", children: _jsx("img", { src: imageSrc, alt: alt, className: "max-w-full h-auto rounded-md border border-border" }) }, getBlockKey()));
|
|
217
|
+
}
|
|
218
|
+
// Handle diff blocks
|
|
219
|
+
if (block.type === "diff" &&
|
|
220
|
+
"path" in block &&
|
|
221
|
+
"oldText" in block &&
|
|
222
|
+
"newText" in block) {
|
|
223
|
+
return (_jsxs("div", { className: "border border-border rounded bg-card", children: [_jsxs("div", { className: "bg-muted px-2 py-1 text-[10px] font-mono text-muted-foreground border-b border-border", children: [block.path, "line" in block &&
|
|
224
|
+
block.line !== null &&
|
|
225
|
+
block.line !== undefined &&
|
|
226
|
+
`:${block.line}`] }), _jsxs("div", { className: "p-2 font-mono text-[11px]", children: [_jsxs("div", { className: "text-red-500 dark:text-red-400", children: ["- ", block.oldText] }), _jsxs("div", { className: "text-green-500 dark:text-green-400", children: ["+ ", block.newText] })] })] }, getBlockKey()));
|
|
227
|
+
}
|
|
228
|
+
// Handle terminal blocks
|
|
229
|
+
if (block.type === "terminal" && "terminalId" in block) {
|
|
230
|
+
return (_jsxs("div", { className: "bg-neutral-900 text-neutral-100 p-2 rounded text-[11px] font-mono", children: ["Terminal: ", block.terminalId] }, getBlockKey()));
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}), toolCall.error && (_jsxs("div", { className: "text-destructive font-mono text-[11px] mt-2", children: ["Error: ", toolCall.error] }))] })] })) : null, toolCall._meta?.truncationWarning && (_jsxs("div", { className: "mx-3 mt-3 mb-0 flex items-center gap-2 rounded-md bg-yellow-50 dark:bg-yellow-950/20 px-3 py-2 text-[11px] text-yellow-800 dark:text-yellow-200 border border-yellow-200 dark:border-yellow-900", children: [_jsx("span", { className: "text-yellow-600 dark:text-yellow-500", children: "\u26A0\uFE0F" }), _jsx("span", { children: toolCall._meta.truncationWarning })] })), (toolCall.tokenUsage || toolCall.startedAt) && (_jsxs("div", { className: "p-2 bg-muted/50 border-t border-border flex flex-wrap gap-4 text-[10px] text-muted-foreground font-sans", children: [toolCall.tokenUsage && (_jsxs("div", { className: "flex gap-3", children: [toolCall.tokenUsage.inputTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Input:" }), toolCall.tokenUsage.inputTokens.toLocaleString()] })), toolCall.tokenUsage.outputTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Output:" }), toolCall.tokenUsage.outputTokens.toLocaleString()] })), toolCall.tokenUsage.totalTokens !== undefined && (_jsxs("div", { children: [_jsx("span", { className: "uppercase tracking-wide font-semibold mr-1", children: "Total:" }), toolCall.tokenUsage.totalTokens.toLocaleString()] }))] })), toolCall.startedAt && (_jsxs("div", { className: "flex gap-3 ml-auto", children: [_jsxs("span", { children: ["Started: ", new Date(toolCall.startedAt).toLocaleTimeString()] }), toolCall.completedAt && (_jsxs("span", { children: ["Completed:", " ", new Date(toolCall.completedAt).toLocaleTimeString(), " (", Math.round((toolCall.completedAt - toolCall.startedAt) / 1000), "s)"] }))] }))] }))] }))] }));
|
|
234
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
|
|
2
|
+
export interface ToolCallGroupProps {
|
|
3
|
+
toolCalls: ToolCallType[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* ToolCallGroup component - displays a group of parallel tool calls with collapsible details
|
|
7
|
+
*/
|
|
8
|
+
export declare function ToolCallGroup({ toolCalls }: ToolCallGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, ListVideo } from "lucide-react";
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { ToolCall } from "./ToolCall.js";
|
|
5
|
+
/**
|
|
6
|
+
* ToolCallGroup component - displays a group of parallel tool calls with collapsible details
|
|
7
|
+
*/
|
|
8
|
+
export function ToolCallGroup({ toolCalls }) {
|
|
9
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
10
|
+
// Calculate group status based on individual tool call statuses
|
|
11
|
+
const getGroupStatus = () => {
|
|
12
|
+
const statuses = toolCalls.map((tc) => tc.status);
|
|
13
|
+
if (statuses.some((s) => s === "failed"))
|
|
14
|
+
return "failed";
|
|
15
|
+
if (statuses.some((s) => s === "in_progress"))
|
|
16
|
+
return "in_progress";
|
|
17
|
+
if (statuses.every((s) => s === "completed"))
|
|
18
|
+
return "completed";
|
|
19
|
+
return "pending";
|
|
20
|
+
};
|
|
21
|
+
const groupStatus = getGroupStatus();
|
|
22
|
+
// Generate summary of tool names
|
|
23
|
+
const toolNames = toolCalls.map((tc) => tc.prettyName || tc.title);
|
|
24
|
+
const uniqueNames = [...new Set(toolNames)];
|
|
25
|
+
const summary = uniqueNames.length <= 2
|
|
26
|
+
? uniqueNames.join(", ")
|
|
27
|
+
: `${uniqueNames.slice(0, 2).join(", ")} +${uniqueNames.length - 2} more`;
|
|
28
|
+
return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: "text-muted-foreground", children: _jsx(ListVideo, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: "Parallel operation" }), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-muted-foreground/70", children: toolCalls.length }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` })] }), !isExpanded && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: summary }))] }), isExpanded && (_jsx("div", { className: "mt-1", children: toolCalls.map((toolCall) => (_jsxs("div", { className: "flex items-start", children: [_jsx("div", { className: "w-2.5 h-4 border-l-2 border-b-2 border-border rounded-bl-[6px] mt-1 mr-0.5 shrink-0" }), _jsx("div", { className: "flex-1 -mt-2", children: _jsx(ToolCall, { toolCall: toolCall }) })] }, toolCall.id))) }))] }));
|
|
29
|
+
}
|
|
@@ -8,6 +8,7 @@ import { generateSmartSummary } from "../../core/utils/tool-summary.js";
|
|
|
8
8
|
import { expandCollapseVariants, fadeInVariants, getDuration, getTransition, motionDuration, motionEasing, rotateVariants, shimmerTransition, standardTransition, } from "../lib/motion.js";
|
|
9
9
|
import { cn } from "../lib/utils.js";
|
|
10
10
|
import * as ChatLayout from "./ChatLayout.js";
|
|
11
|
+
import { SubAgentDetails } from "./SubAgentDetails.js";
|
|
11
12
|
import { useTheme } from "./ThemeProvider.js";
|
|
12
13
|
/**
|
|
13
14
|
* Map of icon names to Lucide components
|
|
@@ -38,6 +39,14 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
38
39
|
// For single tool call, extract it
|
|
39
40
|
const singleToolCall = toolCalls.length === 1 ? toolCalls[0] : null;
|
|
40
41
|
const isTodoWrite = singleToolCall?.title === "todo_write";
|
|
42
|
+
// Detect subagent calls
|
|
43
|
+
const hasLiveSubagent = !!(singleToolCall?.subagentPort && singleToolCall?.subagentSessionId);
|
|
44
|
+
const hasStoredSubagent = !!(singleToolCall?.subagentMessages &&
|
|
45
|
+
singleToolCall.subagentMessages.length > 0);
|
|
46
|
+
const isSubagentCall = hasLiveSubagent || hasStoredSubagent;
|
|
47
|
+
const isReplaySubagent = hasStoredSubagent;
|
|
48
|
+
// State for subagent expansion
|
|
49
|
+
const [isSubagentExpanded, setIsSubagentExpanded] = useState(false);
|
|
41
50
|
// Safely access ChatLayout context
|
|
42
51
|
const layoutContext = React.useContext(ChatLayout.Context);
|
|
43
52
|
// Determine display state
|
|
@@ -75,6 +84,11 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
75
84
|
layoutContext.setActiveTab("todo");
|
|
76
85
|
}
|
|
77
86
|
}
|
|
87
|
+
else if (isSubagentCall) {
|
|
88
|
+
// Toggle subagent expansion
|
|
89
|
+
setUserInteracted(true);
|
|
90
|
+
setIsSubagentExpanded(!isSubagentExpanded);
|
|
91
|
+
}
|
|
78
92
|
else {
|
|
79
93
|
// Normal expand/collapse
|
|
80
94
|
setUserInteracted(true); // Mark as user-interacted to prevent auto-minimize
|
|
@@ -83,7 +97,15 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
83
97
|
setIsMinimized(false);
|
|
84
98
|
}
|
|
85
99
|
}
|
|
86
|
-
}, [
|
|
100
|
+
}, [
|
|
101
|
+
isTodoWrite,
|
|
102
|
+
layoutContext,
|
|
103
|
+
isExpanded,
|
|
104
|
+
isMinimized,
|
|
105
|
+
singleToolCall,
|
|
106
|
+
isSubagentCall,
|
|
107
|
+
isSubagentExpanded,
|
|
108
|
+
]);
|
|
87
109
|
// Get icon for display
|
|
88
110
|
const getIcon = () => {
|
|
89
111
|
if (isGrouped) {
|
|
@@ -166,15 +188,33 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
166
188
|
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
167
189
|
duration: motionDuration.normal,
|
|
168
190
|
ease: motionEasing.smooth,
|
|
169
|
-
}), className: "h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(ChevronDown, { className: "h-3 w-3" }) }))] }), !isGrouped && singleToolCall?.subline && (_jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: singleToolCall.subline })), isGrouped && !isExpanded && (_jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: displayText }))] }), _jsx(AnimatePresence, { initial: false, children: !isTodoWrite && isExpanded && (_jsx(motion.div, { initial: "collapsed", animate: "expanded", exit: "collapsed", variants: expandCollapseVariants, transition: getTransition(shouldReduceMotion ?? false, {
|
|
191
|
+
}), className: "h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(ChevronDown, { className: "h-3 w-3" }) }))] }), !isGrouped && singleToolCall?.subline && (_jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: singleToolCall.subline })), isGrouped && !isExpanded && (_jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: displayText }))] }), !isTodoWrite && isSubagentCall && singleToolCall && (_jsx("div", { className: "pl-4.5 mt-2", children: _jsx(SubAgentDetails, { port: singleToolCall.subagentPort, sessionId: singleToolCall.subagentSessionId, parentStatus: singleToolCall.status, agentName: singleToolCall.rawInput?.agentName, query: singleToolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded, storedMessages: singleToolCall.subagentMessages, isReplay: isReplaySubagent }) })), _jsx(AnimatePresence, { initial: false, children: !isTodoWrite && isExpanded && (_jsx(motion.div, { initial: "collapsed", animate: "expanded", exit: "collapsed", variants: expandCollapseVariants, transition: getTransition(shouldReduceMotion ?? false, {
|
|
170
192
|
duration: motionDuration.normal,
|
|
171
193
|
ease: motionEasing.smooth,
|
|
172
194
|
}), className: "mt-1", children: isGrouped ? (
|
|
173
195
|
// Render individual tool calls in group
|
|
174
|
-
_jsx("div", { className: "flex flex-col gap-4 mt-2", children: toolCalls.map((toolCall) => (_jsx(
|
|
196
|
+
_jsx("div", { className: "flex flex-col gap-4 mt-2", children: toolCalls.map((toolCall) => (_jsx(GroupedToolCallItem, { toolCall: toolCall }, toolCall.id))) })) : (
|
|
175
197
|
// Render single tool call details
|
|
176
198
|
singleToolCall && (_jsx(ToolOperationDetails, { toolCall: singleToolCall }))) }, "expanded-content")) })] }));
|
|
177
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Component to render a single tool call within a grouped parallel operation
|
|
202
|
+
* Handles subagent calls with their own expansion state
|
|
203
|
+
*/
|
|
204
|
+
function GroupedToolCallItem({ toolCall }) {
|
|
205
|
+
const [isSubagentExpanded, setIsSubagentExpanded] = useState(false);
|
|
206
|
+
// Detect subagent calls
|
|
207
|
+
const hasLiveSubagent = !!(toolCall.subagentPort && toolCall.subagentSessionId);
|
|
208
|
+
const hasStoredSubagent = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
|
|
209
|
+
const isSubagentCall = hasLiveSubagent || hasStoredSubagent;
|
|
210
|
+
const isReplaySubagent = hasStoredSubagent;
|
|
211
|
+
if (isSubagentCall) {
|
|
212
|
+
// Render subagent with clickable header and SubAgentDetails component
|
|
213
|
+
return (_jsxs("div", { className: "flex flex-col ml-5", children: [_jsxs("button", { type: "button", className: "flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsSubagentExpanded(!isSubagentExpanded), "aria-expanded": isSubagentExpanded, children: [_jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(CircleDot, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.rawInput?.agentName || "Subagent" }), _jsx(ChevronDown, { className: `h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors transition-transform duration-200 ${isSubagentExpanded ? "rotate-180" : ""}` })] }), _jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, { port: toolCall.subagentPort, sessionId: toolCall.subagentSessionId, parentStatus: toolCall.status, agentName: toolCall.rawInput?.agentName, query: toolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded, storedMessages: toolCall.subagentMessages, isReplay: isReplaySubagent }) })] }));
|
|
214
|
+
}
|
|
215
|
+
// Regular tool call - show details
|
|
216
|
+
return (_jsx("div", { className: "flex items-start gap-1.5", children: _jsx("div", { className: "flex-1 ml-5", children: _jsx(ToolOperationDetails, { toolCall: toolCall }) }) }));
|
|
217
|
+
}
|
|
178
218
|
/**
|
|
179
219
|
* Component to display detailed tool call information
|
|
180
220
|
*/
|
|
@@ -13,9 +13,9 @@ export type MessageRole = z.infer<typeof MessageRole>;
|
|
|
13
13
|
* Content type for messages
|
|
14
14
|
*/
|
|
15
15
|
export declare const ContentType: z.ZodEnum<{
|
|
16
|
-
file: "file";
|
|
17
16
|
text: "text";
|
|
18
17
|
image: "image";
|
|
18
|
+
file: "file";
|
|
19
19
|
tool_call: "tool_call";
|
|
20
20
|
tool_result: "tool_result";
|
|
21
21
|
}>;
|
|
@@ -25,9 +25,9 @@ export type ContentType = z.infer<typeof ContentType>;
|
|
|
25
25
|
*/
|
|
26
26
|
export declare const BaseContent: z.ZodObject<{
|
|
27
27
|
type: z.ZodEnum<{
|
|
28
|
-
file: "file";
|
|
29
28
|
text: "text";
|
|
30
29
|
image: "image";
|
|
30
|
+
file: "file";
|
|
31
31
|
tool_call: "tool_call";
|
|
32
32
|
tool_result: "tool_result";
|
|
33
33
|
}>;
|
|
@@ -4,10 +4,10 @@ import { z } from "zod";
|
|
|
4
4
|
*/
|
|
5
5
|
export declare const SessionStatus: z.ZodEnum<{
|
|
6
6
|
error: "error";
|
|
7
|
-
active: "active";
|
|
8
7
|
idle: "idle";
|
|
9
8
|
connecting: "connecting";
|
|
10
9
|
connected: "connected";
|
|
10
|
+
active: "active";
|
|
11
11
|
streaming: "streaming";
|
|
12
12
|
disconnected: "disconnected";
|
|
13
13
|
}>;
|
|
@@ -41,10 +41,10 @@ export declare const Session: z.ZodObject<{
|
|
|
41
41
|
id: z.ZodString;
|
|
42
42
|
status: z.ZodEnum<{
|
|
43
43
|
error: "error";
|
|
44
|
-
active: "active";
|
|
45
44
|
idle: "idle";
|
|
46
45
|
connecting: "connecting";
|
|
47
46
|
connected: "connected";
|
|
47
|
+
active: "active";
|
|
48
48
|
streaming: "streaming";
|
|
49
49
|
disconnected: "disconnected";
|
|
50
50
|
}>;
|
|
@@ -117,10 +117,10 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
117
117
|
sessionId: z.ZodString;
|
|
118
118
|
status: z.ZodOptional<z.ZodEnum<{
|
|
119
119
|
error: "error";
|
|
120
|
-
active: "active";
|
|
121
120
|
idle: "idle";
|
|
122
121
|
connecting: "connecting";
|
|
123
122
|
connected: "connected";
|
|
123
|
+
active: "active";
|
|
124
124
|
streaming: "streaming";
|
|
125
125
|
disconnected: "disconnected";
|
|
126
126
|
}>>;
|
|
@@ -354,10 +354,10 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
354
354
|
sessionId: z.ZodString;
|
|
355
355
|
status: z.ZodOptional<z.ZodEnum<{
|
|
356
356
|
error: "error";
|
|
357
|
-
active: "active";
|
|
358
357
|
idle: "idle";
|
|
359
358
|
connecting: "connecting";
|
|
360
359
|
connected: "connected";
|
|
360
|
+
active: "active";
|
|
361
361
|
streaming: "streaming";
|
|
362
362
|
disconnected: "disconnected";
|
|
363
363
|
}>>;
|
|
@@ -561,10 +561,10 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
561
561
|
sessionId: z.ZodString;
|
|
562
562
|
status: z.ZodOptional<z.ZodEnum<{
|
|
563
563
|
error: "error";
|
|
564
|
-
active: "active";
|
|
565
564
|
idle: "idle";
|
|
566
565
|
connecting: "connecting";
|
|
567
566
|
connected: "connected";
|
|
567
|
+
active: "active";
|
|
568
568
|
streaming: "streaming";
|
|
569
569
|
disconnected: "disconnected";
|
|
570
570
|
}>>;
|
|
@@ -626,10 +626,10 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
626
626
|
sessionId: z.ZodString;
|
|
627
627
|
status: z.ZodOptional<z.ZodEnum<{
|
|
628
628
|
error: "error";
|
|
629
|
-
active: "active";
|
|
630
629
|
idle: "idle";
|
|
631
630
|
connecting: "connecting";
|
|
632
631
|
connected: "connected";
|
|
632
|
+
active: "active";
|
|
633
633
|
streaming: "streaming";
|
|
634
634
|
disconnected: "disconnected";
|
|
635
635
|
}>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.70",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"@radix-ui/react-slot": "^1.2.4",
|
|
50
50
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
51
51
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
52
|
-
"@townco/core": "0.0.
|
|
52
|
+
"@townco/core": "0.0.48",
|
|
53
53
|
"@uiw/react-json-view": "^2.0.0-alpha.39",
|
|
54
54
|
"bun": "^1.3.1",
|
|
55
55
|
"class-variance-authority": "^0.7.1",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@tailwindcss/postcss": "^4.1.17",
|
|
70
|
-
"@townco/tsconfig": "0.1.
|
|
70
|
+
"@townco/tsconfig": "0.1.67",
|
|
71
71
|
"@types/node": "^24.10.0",
|
|
72
72
|
"@types/react": "^19.2.2",
|
|
73
73
|
"ink": "^6.4.0",
|