@townco/ui 0.1.69 → 0.1.71
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.
|
@@ -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) => {
|
|
@@ -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)))
|
|
@@ -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
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.71",
|
|
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.49",
|
|
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.68",
|
|
71
71
|
"@types/node": "^24.10.0",
|
|
72
72
|
"@types/react": "^19.2.2",
|
|
73
73
|
"ink": "^6.4.0",
|