@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
- // If this is a programmatic scroll, don't update wasAtBottomRef
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 was at bottom
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
- // If user was at the bottom, scroll to new content
116
- if (wasAtBottomRef.current && !isAutoScrollingRef.current) {
117
- // Use requestAnimationFrame to ensure DOM has updated
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
- scrollToBottom(false); // Use instant scroll for streaming to avoid jarring smooth animations
106
+ container.scrollTop = container.scrollHeight;
120
107
  });
121
108
  }
122
- // Update scroll position state (but don't change wasAtBottomRef if we're auto-scrolling)
123
- if (!isAutoScrollingRef.current) {
124
- checkScrollPosition();
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
- const scrollToBottomInstant = () => {
139
- container.scrollTop = container.scrollHeight;
140
- wasAtBottomRef.current = true;
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
- }, 100);
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
- }, [checkScrollPosition]);
202
- // Detect user interaction with scroll area (wheel, touch) - IMMEDIATELY break auto-scroll
203
- const handleUserInteraction = React.useCallback(() => {
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
- const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
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: "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.contentBlocks &&
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
- }, [isTodoWrite, layoutContext, isExpanded, isMinimized, singleToolCall]);
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("div", { className: "flex items-start gap-1.5", children: _jsx("div", { className: "flex-1 ml-5", children: _jsx(ToolOperationDetails, { toolCall: toolCall }) }) }, toolCall.id))) })) : (
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.69",
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.47",
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.66",
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",