@townco/ui 0.1.105 → 0.1.107

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.
@@ -160,6 +160,7 @@ export declare function useChatMessages(client: AcpClient | null, startSession:
160
160
  isStreaming?: boolean | undefined;
161
161
  }[] | undefined;
162
162
  subagentStreaming?: boolean | undefined;
163
+ subagentCompleted?: boolean | undefined;
163
164
  }[] | undefined;
164
165
  hookNotifications?: {
165
166
  id: string;
@@ -76,12 +76,28 @@ export function useChatSession(client, initialSessionId) {
76
76
  // Check if we should append to the last assistant message or create a new one
77
77
  const messages = useChatStore.getState().messages;
78
78
  const lastMessage = messages[messages.length - 1];
79
+ // Check if this is a replay chunk (not live streaming)
80
+ const isReplayChunk = updateMeta?.isReplay === true;
79
81
  if (update.message.role === "assistant" &&
82
+ lastMessage?.role === "assistant" &&
83
+ textContent &&
84
+ isReplayChunk) {
85
+ // During replay, always append to the last assistant message
86
+ // This allows multi-part messages (text, tools, text) to accumulate correctly
87
+ logger.debug("Appending replay text to existing assistant message", {
88
+ existingLength: lastMessage.content.length,
89
+ appendingLength: textContent.length,
90
+ });
91
+ useChatStore.getState().updateMessage(lastMessage.id, {
92
+ content: lastMessage.content + textContent,
93
+ });
94
+ }
95
+ else if (update.message.role === "assistant" &&
80
96
  lastMessage?.role === "assistant" &&
81
97
  lastMessage.content === "" &&
82
98
  textContent) {
83
- // Append text to existing empty assistant message (created by tool call)
84
- logger.debug("Appending text to existing assistant message");
99
+ // Non-replay: append text to existing empty assistant message (created by tool call)
100
+ logger.debug("Appending text to existing empty assistant message");
85
101
  useChatStore.getState().updateMessage(lastMessage.id, {
86
102
  content: lastMessage.content + textContent,
87
103
  });
@@ -157,6 +157,7 @@ export declare function useToolCalls(client: AcpClient | null): {
157
157
  isStreaming?: boolean | undefined;
158
158
  }[] | undefined;
159
159
  subagentStreaming?: boolean | undefined;
160
+ subagentCompleted?: boolean | undefined;
160
161
  }[]>;
161
162
  getToolCallsForSession: (sessionId: string) => {
162
163
  id: string;
@@ -307,5 +308,6 @@ export declare function useToolCalls(client: AcpClient | null): {
307
308
  isStreaming?: boolean | undefined;
308
309
  }[] | undefined;
309
310
  subagentStreaming?: boolean | undefined;
311
+ subagentCompleted?: boolean | undefined;
310
312
  }[];
311
313
  };
@@ -28,22 +28,10 @@ export function useToolCalls(client) {
28
28
  addToolCallToCurrentMessage(update.toolCall);
29
29
  }
30
30
  else if (update.type === "tool_call_update") {
31
- // Tool call update notification
32
- _logger.info("[SUBAGENT] Frontend received tool_call_update", {
33
- sessionId: update.sessionId,
34
- toolCallId: update.toolCallUpdate.id,
35
- status: update.toolCallUpdate.status,
36
- hasSubagentMessages: !!update.toolCallUpdate.subagentMessages,
37
- subagentMessageCount: update.toolCallUpdate.subagentMessages?.length || 0,
38
- });
39
31
  // Update session-level tool calls (for sidebar)
40
32
  updateToolCall(update.sessionId, update.toolCallUpdate);
41
33
  // Also update in current assistant message (for inline display)
42
34
  updateToolCallInCurrentMessage(update.toolCallUpdate);
43
- _logger.info("[SUBAGENT] Successfully updated tool call state", {
44
- sessionId: update.sessionId,
45
- toolCallId: update.toolCallUpdate.id,
46
- });
47
35
  }
48
36
  });
49
37
  return () => {
@@ -239,6 +239,7 @@ export declare const DisplayMessage: z.ZodObject<{
239
239
  isStreaming: z.ZodOptional<z.ZodBoolean>;
240
240
  }, z.core.$strip>>>;
241
241
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
242
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
242
243
  }, z.core.$strip>>>;
243
244
  hookNotifications: z.ZodOptional<z.ZodArray<z.ZodObject<{
244
245
  id: z.ZodString;
@@ -503,6 +504,7 @@ export declare const ChatSessionState: z.ZodObject<{
503
504
  isStreaming: z.ZodOptional<z.ZodBoolean>;
504
505
  }, z.core.$strip>>>;
505
506
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
507
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
506
508
  }, z.core.$strip>>>;
507
509
  hookNotifications: z.ZodOptional<z.ZodArray<z.ZodObject<{
508
510
  id: z.ZodString;
@@ -445,6 +445,7 @@ export declare const ToolCallSchema: z.ZodObject<{
445
445
  isStreaming: z.ZodOptional<z.ZodBoolean>;
446
446
  }, z.core.$strip>>>;
447
447
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
448
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
448
449
  }, z.core.$strip>;
449
450
  export type ToolCall = z.infer<typeof ToolCallSchema>;
450
451
  /**
@@ -596,6 +597,7 @@ export declare const ToolCallUpdateSchema: z.ZodObject<{
596
597
  }, z.core.$strip>], "type">>>;
597
598
  isStreaming: z.ZodOptional<z.ZodBoolean>;
598
599
  }, z.core.$strip>>>;
600
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
599
601
  _meta: z.ZodOptional<z.ZodObject<{
600
602
  truncationWarning: z.ZodOptional<z.ZodString>;
601
603
  compactionAction: z.ZodOptional<z.ZodEnum<{
@@ -183,6 +183,8 @@ export const ToolCallSchema = z.object({
183
183
  subagentMessages: z.array(SubagentMessageSchema).optional(),
184
184
  /** Whether the sub-agent is currently streaming */
185
185
  subagentStreaming: z.boolean().optional(),
186
+ /** Whether the sub-agent has completed */
187
+ subagentCompleted: z.boolean().optional(),
186
188
  });
187
189
  /**
188
190
  * Partial update for an existing tool call
@@ -207,6 +209,8 @@ export const ToolCallUpdateSchema = z.object({
207
209
  subagentSessionId: z.string().optional(),
208
210
  /** Sub-agent messages for replay */
209
211
  subagentMessages: z.array(SubagentMessageSchema).optional(),
212
+ /** Whether the sub-agent has completed */
213
+ subagentCompleted: z.boolean().optional(),
210
214
  /** Internal metadata (e.g., compaction info) */
211
215
  _meta: z
212
216
  .object({
@@ -245,6 +249,8 @@ export function mergeToolCallUpdate(existing, update) {
245
249
  subagentSessionId: update.subagentSessionId ?? existing.subagentSessionId,
246
250
  // Sub-agent messages for replay
247
251
  subagentMessages: update.subagentMessages ?? existing.subagentMessages,
252
+ // Sub-agent completion status (once true, stays true)
253
+ subagentCompleted: update.subagentCompleted ?? existing.subagentCompleted,
248
254
  // Internal metadata (compaction info)
249
255
  _meta: update._meta ?? existing._meta,
250
256
  };
@@ -265,10 +265,12 @@ export const useChatStore = create((set) => ({
265
265
  if (!lastAssistantMsg)
266
266
  return state;
267
267
  // Track the content position where this tool call was invoked
268
- const contentPosition = lastAssistantMsg.content.length;
268
+ // Set contentPosition to where the tool call was invoked in the content stream
269
+ // This ensures tool calls appear inline at the correct position, even during streaming
270
+ const currentContentLength = lastAssistantMsg.content.length;
269
271
  const toolCallWithPosition = {
270
272
  ...toolCall,
271
- contentPosition,
273
+ contentPosition: currentContentLength,
272
274
  };
273
275
  messages[lastAssistantIndex] = {
274
276
  ...lastAssistantMsg,
@@ -300,7 +302,8 @@ export const useChatStore = create((set) => ({
300
302
  if (!existing)
301
303
  return state;
302
304
  const updatedToolCalls = [...toolCalls];
303
- updatedToolCalls[existingIndex] = mergeToolCallUpdate(existing, update);
305
+ const merged = mergeToolCallUpdate(existing, update);
306
+ updatedToolCalls[existingIndex] = merged;
304
307
  messages[lastAssistantIndex] = {
305
308
  ...lastAssistantMsg,
306
309
  toolCalls: updatedToolCalls,
@@ -14,7 +14,14 @@ export function getToolCallDisplayState(toolCall) {
14
14
  if (toolCall.status === "failed" || toolCall.error) {
15
15
  return "failed";
16
16
  }
17
- // Completed state
17
+ // Check if this is a subagent call
18
+ const hasSubagent = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
19
+ // For subagent calls: completed when subagentCompleted is true (regardless of status)
20
+ // This handles the case where status is "in_progress" but subagent has finished
21
+ if (hasSubagent && toolCall.subagentCompleted) {
22
+ return "completed";
23
+ }
24
+ // Completed state (for non-subagent calls)
18
25
  if (toolCall.status === "completed") {
19
26
  return "completed";
20
27
  }
@@ -2,7 +2,7 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
2
2
  import { cva } from "class-variance-authority";
3
3
  import * as React from "react";
4
4
  import { useChatStore } from "../../core/store/chat-store.js";
5
- import { isPreliminaryToolCall } from "../../core/utils/tool-call-state.js";
5
+ import { getToolCallDisplayState, isPreliminaryToolCall, } from "../../core/utils/tool-call-state.js";
6
6
  import { cn } from "../lib/utils.js";
7
7
  import { HookNotification } from "./HookNotification.js";
8
8
  import { Reasoning } from "./Reasoning.js";
@@ -140,6 +140,19 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
140
140
  };
141
141
  // Get tool_response hook notifications to associate with tool calls
142
142
  const toolResponseHooks = (message.hookNotifications || []).filter((n) => n.hookType === "tool_response");
143
+ // Helper to check if all non-preliminary tool calls are completed
144
+ const areAllToolCallsCompleted = () => {
145
+ const nonPreliminary = sortedToolCalls.filter((tc) => !isPreliminaryToolCall(tc));
146
+ if (nonPreliminary.length === 0)
147
+ return false;
148
+ return nonPreliminary.every((tc) => getToolCallDisplayState(tc) === "completed");
149
+ };
150
+ // Check if we should show the thinking indicator
151
+ // Show when: all tool calls are complete, message is streaming, and there's minimal content after tool calls
152
+ const shouldShowThinkingIndicator = message.isStreaming &&
153
+ sortedToolCalls.length > 0 &&
154
+ areAllToolCallsCompleted() &&
155
+ message.content.trim().length < 50; // Only show if there's very little text content
143
156
  // Helper to render a tool call or group
144
157
  const renderToolCallOrGroup = (item, index) => {
145
158
  // Batch group (parallel operations)
@@ -175,9 +188,8 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
175
188
  // Check if hook notifications have positions for inline rendering
176
189
  const hasHookPositions = hookNotifications.some((n) => n.contentPosition !== undefined);
177
190
  if (!hasHookPositions) {
178
- // No positions - render hooks at top, then tool calls, then content
179
- return (_jsxs(_Fragment, { children: [hookNotifications.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: hookNotifications.map((notification) => (_jsx(HookNotification, { notification: notification }, notification.id))) })), groupedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) })), _jsx("div", { children: _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false, sources: message.sources ?? [] }) })
180
- ] }));
191
+ // No positions - render hooks at top, then content, then tool calls
192
+ return (_jsxs(_Fragment, { children: [hookNotifications.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: hookNotifications.map((notification) => (_jsx(HookNotification, { notification: notification }, notification.id))) })), _jsx("div", { children: _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false, sources: message.sources ?? [] }) }), groupedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) })), shouldShowThinkingIndicator && (_jsx("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: _jsx("div", { className: "flex items-center gap-1.5", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Thinking..." }) }) }))] }));
181
193
  }
182
194
  // Hooks have positions - render them inline with content
183
195
  const elements = [];
@@ -336,6 +348,10 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
336
348
  toolCalls: [preliminaryToolCalls[0]], isGrouped: false }) }, `tool-${preliminaryToolCalls[0]?.id}`));
337
349
  }
338
350
  }
351
+ // Add thinking indicator if all tool calls are complete but message is still streaming
352
+ if (shouldShowThinkingIndicator) {
353
+ elements.push(_jsx("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: _jsx("div", { className: "flex items-center gap-1.5", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Thinking..." }) }) }, "thinking-indicator"));
354
+ }
339
355
  return _jsx(_Fragment, { children: elements });
340
356
  })()) : (_jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: message.images.map((image, imageIndex) => (_jsx("img", { src: `data:${image.mimeType};base64,${image.data}`, alt: `Attachment ${imageIndex + 1}`, className: "max-w-[200px] max-h-[200px] rounded-lg object-cover" }, `image-${image.mimeType}-${image.data.slice(0, 20)}`))) })), message.content && (_jsx("div", { className: "whitespace-pre-wrap", children: message.content }))] }))] }));
341
357
  }
@@ -23,18 +23,6 @@ export function SubAgentDetails({ parentStatus, agentName, query, isExpanded: co
23
23
  const thinkingContainerRef = useRef(null);
24
24
  // Use messages from storedMessages prop (populated by parent via tool_call_update)
25
25
  const messages = storedMessages || [];
26
- // Log when messages are received/updated
27
- useEffect(() => {
28
- console.log("[SUBAGENT] SubAgentDetails received messages update", {
29
- agentName,
30
- messageCount: messages.length,
31
- parentStatus,
32
- hasMessages: messages.length > 0,
33
- contentPreview: messages[0]?.content?.substring(0, 100),
34
- toolCallCount: messages[0]?.toolCalls?.length || 0,
35
- contentBlockCount: messages[0]?.contentBlocks?.length || 0,
36
- });
37
- }, [messages, agentName, parentStatus]);
38
26
  // Determine if subagent is still running based on parent status
39
27
  const isRunning = parentStatus === "in_progress" || parentStatus === "pending";
40
28
  // Get the current/latest message
@@ -44,13 +32,6 @@ export function SubAgentDetails({ parentStatus, agentName, query, isExpanded: co
44
32
  (currentMessage.toolCalls && currentMessage.toolCalls.length > 0) ||
45
33
  (currentMessage.contentBlocks &&
46
34
  currentMessage.contentBlocks.length > 0));
47
- console.log("[SUBAGENT] SubAgentDetails render state", {
48
- agentName,
49
- messageCount: messages.length,
50
- hasContent,
51
- isRunning,
52
- parentStatus,
53
- });
54
35
  // Auto-collapse Thinking when completed (so Output is the primary view)
55
36
  const prevIsRunningRef = useRef(isRunning);
56
37
  useEffect(() => {
@@ -71,8 +71,8 @@ function formatElapsedTime(seconds) {
71
71
  /**
72
72
  * Component to display running duration for a tool call
73
73
  */
74
- function RunningDuration({ startTime }) {
75
- const elapsed = useElapsedTime(startTime, true);
74
+ function RunningDuration({ startTime, isRunning = true, }) {
75
+ const elapsed = useElapsedTime(startTime, isRunning);
76
76
  return (_jsx("span", { className: "text-xs text-text-secondary/70 tabular-nums ml-1", children: formatElapsedTime(elapsed) }));
77
77
  }
78
78
  /**
@@ -227,7 +227,7 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
227
227
  // For preliminary/selecting states, show simple non-expandable text
228
228
  if (isSelecting && !isGrouped) {
229
229
  return (_jsx("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: _jsxs("div", { className: "flex items-center gap-1.5", children: [
230
- _jsx("div", { className: "text-text-secondary/70", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-xs text-text-muted", children: [displayText, singleToolCall?.startedAt && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt }))] })
230
+ _jsx("div", { className: "text-text-secondary/70", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-xs text-text-muted", children: [displayText, singleToolCall?.startedAt && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt, isRunning: !singleToolCall.subagentCompleted }))] })
231
231
  ] }) }));
232
232
  }
233
233
  // If it's a grouped preliminary (selecting) state
@@ -241,9 +241,11 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
241
241
  return null;
242
242
  return Math.min(...startTimes);
243
243
  })();
244
+ // Check if all tool calls in the group are completed
245
+ const allCompleted = toolCalls.every((tc) => tc.subagentCompleted);
244
246
  return (_jsxs("div", { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", children: [
245
247
  _jsxs("div", { className: "flex items-center gap-1.5", children: [
246
- _jsx("div", { className: "text-text-secondary/70", children: _jsx(ListVideo, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Selecting tools" }), selectingStartTime && (_jsx(RunningDuration, { startTime: selectingStartTime })), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })
248
+ _jsx("div", { className: "text-text-secondary/70", children: _jsx(ListVideo, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Selecting tools" }), selectingStartTime && (_jsx(RunningDuration, { startTime: selectingStartTime, isRunning: !allCompleted })), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })
247
249
  ] }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: displayText })
248
250
  ] }));
249
251
  }
@@ -253,7 +255,7 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
253
255
  _jsxs("div", { className: "flex items-center gap-1.5", children: [
254
256
  _jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: [isGrouped && _jsx("span", { className: "mr-1", children: "Parallel operation" }), !isGrouped && displayText] }), !isGrouped &&
255
257
  singleToolCall?.startedAt &&
256
- displayState === "executing" && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt })), isGrouped &&
258
+ displayState === "executing" && (_jsx(RunningDuration, { startTime: singleToolCall.startedAt, isRunning: !singleToolCall.subagentCompleted })), isGrouped &&
257
259
  displayState === "executing" &&
258
260
  (() => {
259
261
  const startTimes = toolCalls
@@ -262,7 +264,10 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
262
264
  if (startTimes.length === 0)
263
265
  return null;
264
266
  const earliestStart = Math.min(...startTimes);
265
- return _jsx(RunningDuration, { startTime: earliestStart });
267
+ // Check if all tool calls are completed
268
+ const allCompleted = toolCalls.every((tc) => tc.status === "completed" &&
269
+ (!tc.subagentMessages || tc.subagentCompleted));
270
+ return (_jsx(RunningDuration, { startTime: earliestStart, isRunning: !allCompleted }));
266
271
  })(), isGrouped && (_jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-text-secondary/70", children: toolCalls.length })), isFailed && (_jsx("span", { title: isGrouped
267
272
  ? `${toolCalls.filter((tc) => tc.status === "failed").length} of ${toolCalls.length} operations failed`
268
273
  : singleToolCall?.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), isGrouped && groupHasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
@@ -338,7 +343,8 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
338
343
  hookNotification?.metadata?.action === "compacted_then_truncated" ||
339
344
  toolCall._meta?.compactionAction === "truncated");
340
345
  const isFailed = toolCall.status === "failed";
341
- const isRunning = toolCall.status === "pending" || toolCall.status === "in_progress";
346
+ const isRunning = (toolCall.status === "pending" || toolCall.status === "in_progress") &&
347
+ !toolCall.subagentCompleted;
342
348
  if (isSubagentCall) {
343
349
  // Render subagent with clickable header and SubAgentDetails component
344
350
  return (_jsxs("div", { className: "flex flex-col ml-5", children: [
@@ -350,6 +350,7 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
350
350
  isStreaming: z.ZodOptional<z.ZodBoolean>;
351
351
  }, z.core.$strip>>>;
352
352
  subagentStreaming: z.ZodOptional<z.ZodBoolean>;
353
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
353
354
  }, z.core.$strip>;
354
355
  messageId: z.ZodOptional<z.ZodString>;
355
356
  }, z.core.$strip>, z.ZodObject<{
@@ -557,6 +558,7 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
557
558
  }, z.core.$strip>], "type">>>;
558
559
  isStreaming: z.ZodOptional<z.ZodBoolean>;
559
560
  }, z.core.$strip>>>;
561
+ subagentCompleted: z.ZodOptional<z.ZodBoolean>;
560
562
  _meta: z.ZodOptional<z.ZodObject<{
561
563
  truncationWarning: z.ZodOptional<z.ZodString>;
562
564
  compactionAction: z.ZodOptional<z.ZodEnum<{
@@ -1169,6 +1169,17 @@ export class HttpTransport {
1169
1169
  typeof update._meta.originalContentPath === "string"
1170
1170
  ? update._meta.originalContentPath
1171
1171
  : undefined;
1172
+ // Check for subagentCompleted at top level first (sent by adapter),
1173
+ // then fallback to _meta (for backward compatibility)
1174
+ const subagentCompleted = "subagentCompleted" in update &&
1175
+ typeof update.subagentCompleted === "boolean"
1176
+ ? update.subagentCompleted
1177
+ : update._meta &&
1178
+ typeof update._meta === "object" &&
1179
+ "subagentCompleted" in update._meta &&
1180
+ typeof update._meta.subagentCompleted === "boolean"
1181
+ ? update._meta.subagentCompleted
1182
+ : undefined;
1172
1183
  // Tool call update notification
1173
1184
  const toolCallUpdate = {
1174
1185
  id: update.toolCallId ?? "",
@@ -1181,6 +1192,7 @@ export class HttpTransport {
1181
1192
  locations: update.locations,
1182
1193
  rawOutput: update.rawOutput,
1183
1194
  tokenUsage: update.tokenUsage,
1195
+ subagentCompleted,
1184
1196
  content: update.content?.map((c) => {
1185
1197
  // Type guard to safely check properties
1186
1198
  if (typeof c !== "object" || c === null) {
@@ -1492,8 +1504,9 @@ export class HttpTransport {
1492
1504
  isComplete: false,
1493
1505
  };
1494
1506
  }
1495
- // Only queue chunks for live streaming, not replay
1496
- if (chunk && !isReplay) {
1507
+ // Queue chunks for both live streaming AND replay
1508
+ // Replay chunks need to be accumulated just like live chunks
1509
+ if (chunk) {
1497
1510
  // Resolve any waiting receive() calls immediately
1498
1511
  const resolver = this.chunkResolvers.shift();
1499
1512
  if (resolver) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.105",
3
+ "version": "0.1.107",
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.83",
52
+ "@townco/core": "0.0.85",
53
53
  "@types/mdast": "^4.0.4",
54
54
  "@uiw/react-json-view": "^2.0.0-alpha.39",
55
55
  "class-variance-authority": "^0.7.1",
@@ -67,19 +67,19 @@
67
67
  "zustand": "^5.0.8"
68
68
  },
69
69
  "devDependencies": {
70
- "@townco/tsconfig": "0.1.102",
70
+ "@townco/tsconfig": "0.1.104",
71
71
  "@types/node": "^24.10.0",
72
72
  "@types/react": "^19.2.2",
73
73
  "@types/unist": "^3.0.3",
74
74
  "@typescript/native-preview": "^7.0.0-dev.20251207.1",
75
75
  "ink": "^6.4.0",
76
- "react": "19.2.1",
76
+ "react": "19.2.3",
77
77
  "tailwindcss": "^4.1.17"
78
78
  },
79
79
  "peerDependencies": {
80
80
  "ink": "^6.4.0",
81
- "react": "^19.2.1",
82
- "react-dom": "^19.2.1"
81
+ "react": "^19.2.3",
82
+ "react-dom": "^19.2.3"
83
83
  },
84
84
  "peerDependenciesMeta": {
85
85
  "react": {
@@ -1,28 +0,0 @@
1
- import type { SubagentMessage } from "../schemas/tool-call.js";
2
- export interface UseSubagentStreamOptions {
3
- /** Sub-agent HTTP port */
4
- port: number;
5
- /** Sub-agent session ID */
6
- sessionId: string;
7
- /** Base host (defaults to localhost) */
8
- host?: string;
9
- }
10
- export interface UseSubagentStreamReturn {
11
- /** Accumulated messages from the sub-agent */
12
- messages: SubagentMessage[];
13
- /** Whether the stream is currently active */
14
- isStreaming: boolean;
15
- /** Error message if connection failed */
16
- error: string | null;
17
- }
18
- /**
19
- * Hook to connect directly to a sub-agent's SSE endpoint and stream messages.
20
- *
21
- * This hook:
22
- * - Connects to the sub-agent's HTTP server at the given port
23
- * - Subscribes to the /events SSE endpoint with the session ID
24
- * - Parses incoming session/update notifications
25
- * - Extracts text chunks and tool calls
26
- * - Returns accumulated messages for display
27
- */
28
- export declare function useSubagentStream(options: UseSubagentStreamOptions | null): UseSubagentStreamReturn;
@@ -1,256 +0,0 @@
1
- import { createLogger } from "@townco/core";
2
- import { useCallback, useEffect, useRef, useState } from "react";
3
- const logger = createLogger("subagent-stream");
4
- /**
5
- * Hook to connect directly to a sub-agent's SSE endpoint and stream messages.
6
- *
7
- * This hook:
8
- * - Connects to the sub-agent's HTTP server at the given port
9
- * - Subscribes to the /events SSE endpoint with the session ID
10
- * - Parses incoming session/update notifications
11
- * - Extracts text chunks and tool calls
12
- * - Returns accumulated messages for display
13
- */
14
- export function useSubagentStream(options) {
15
- const [messages, setMessages] = useState([]);
16
- // Start as streaming=true if options provided, since we're about to connect
17
- const [_isStreaming, setIsStreaming] = useState(!!options);
18
- const [hasCompleted, setHasCompleted] = useState(false);
19
- const [error, setError] = useState(null);
20
- const abortControllerRef = useRef(null);
21
- const currentMessageRef = useRef(null);
22
- const updateTimeoutRef = useRef(null);
23
- // Throttled update to prevent excessive re-renders
24
- const scheduleUpdate = useCallback(() => {
25
- // If there's already a pending timeout, let it handle the update
26
- // (it will read the latest currentMessageRef value)
27
- if (updateTimeoutRef.current)
28
- return;
29
- updateTimeoutRef.current = setTimeout(() => {
30
- updateTimeoutRef.current = null;
31
- if (currentMessageRef.current) {
32
- setMessages([{ ...currentMessageRef.current }]);
33
- }
34
- }, 250); // Batch updates every 250ms
35
- }, []);
36
- // Process incoming SSE message from sub-agent
37
- // Defined BEFORE connectToSubagent so it's available in the closure
38
- const processSSEMessage = useCallback((data) => {
39
- try {
40
- const message = JSON.parse(data);
41
- logger.debug("Processing SSE message", {
42
- method: message.method,
43
- hasParams: !!message.params,
44
- });
45
- // Check if this is a session/update notification
46
- if (message.method === "session/update" && message.params?.update) {
47
- const update = message.params.update;
48
- logger.debug("Got session update", {
49
- sessionUpdate: update.sessionUpdate,
50
- });
51
- if (update.sessionUpdate === "agent_message_chunk") {
52
- // Handle text chunk
53
- const content = update.content;
54
- if (content?.type === "text" && typeof content.text === "string") {
55
- if (currentMessageRef.current) {
56
- currentMessageRef.current.content += content.text;
57
- // Add to contentBlocks - append to last text block or create new one
58
- const blocks = currentMessageRef.current.contentBlocks ?? [];
59
- const lastBlock = blocks[blocks.length - 1];
60
- if (lastBlock && lastBlock.type === "text") {
61
- lastBlock.text += content.text;
62
- }
63
- else {
64
- blocks.push({ type: "text", text: content.text });
65
- }
66
- currentMessageRef.current.contentBlocks = blocks;
67
- scheduleUpdate();
68
- }
69
- }
70
- }
71
- else if (update.sessionUpdate === "tool_call") {
72
- // Handle new tool call
73
- const toolCall = {
74
- id: update.toolCallId ?? `tc-${Date.now()}`,
75
- title: update.title ?? "Tool call",
76
- prettyName: update._meta?.prettyName,
77
- icon: update._meta?.icon,
78
- status: update.status ?? "pending",
79
- content: [],
80
- };
81
- if (currentMessageRef.current) {
82
- currentMessageRef.current.toolCalls = [
83
- ...(currentMessageRef.current.toolCalls ?? []),
84
- toolCall,
85
- ];
86
- // Add to contentBlocks for interleaved display
87
- const blocks = currentMessageRef.current.contentBlocks ?? [];
88
- blocks.push({ type: "tool_call", toolCall });
89
- currentMessageRef.current.contentBlocks = blocks;
90
- scheduleUpdate();
91
- }
92
- }
93
- else if (update.sessionUpdate === "tool_call_update") {
94
- // Handle tool call update (status change, completion)
95
- if (currentMessageRef.current?.toolCalls) {
96
- const toolCallId = update.toolCallId;
97
- const updateToolCall = (tc) => tc.id === toolCallId
98
- ? {
99
- ...tc,
100
- status: update.status ?? tc.status,
101
- content: update.content ?? tc.content,
102
- }
103
- : tc;
104
- currentMessageRef.current.toolCalls =
105
- currentMessageRef.current.toolCalls.map(updateToolCall);
106
- // Also update in contentBlocks
107
- if (currentMessageRef.current.contentBlocks) {
108
- currentMessageRef.current.contentBlocks =
109
- currentMessageRef.current.contentBlocks.map((block) => block.type === "tool_call"
110
- ? { ...block, toolCall: updateToolCall(block.toolCall) }
111
- : block);
112
- }
113
- scheduleUpdate();
114
- }
115
- }
116
- }
117
- }
118
- catch (err) {
119
- logger.error("Failed to parse sub-agent SSE message", {
120
- error: err instanceof Error ? err.message : String(err),
121
- });
122
- }
123
- }, [scheduleUpdate]);
124
- const connectToSubagent = useCallback(async (port, sessionId, host, protocol) => {
125
- const baseUrl = `${protocol}//${host}:${port}`;
126
- logger.info("Connecting to sub-agent SSE", { baseUrl, sessionId });
127
- setIsStreaming(true);
128
- setError(null);
129
- // Create abort controller for cleanup
130
- const abortController = new AbortController();
131
- abortControllerRef.current = abortController;
132
- try {
133
- logger.info("Fetching SSE endpoint", {
134
- url: `${baseUrl}/events`,
135
- sessionId,
136
- });
137
- const response = await fetch(`${baseUrl}/events`, {
138
- method: "GET",
139
- headers: {
140
- "X-Session-ID": sessionId,
141
- },
142
- signal: abortController.signal,
143
- });
144
- logger.info("SSE response received", {
145
- status: response.status,
146
- ok: response.ok,
147
- });
148
- if (!response.ok) {
149
- throw new Error(`SSE connection failed: HTTP ${response.status}`);
150
- }
151
- if (!response.body) {
152
- throw new Error("Response body is null");
153
- }
154
- logger.info("Sub-agent SSE connection opened, starting to read stream");
155
- // Read the SSE stream
156
- const reader = response.body.getReader();
157
- const decoder = new TextDecoder();
158
- let buffer = "";
159
- // Initialize current message
160
- currentMessageRef.current = {
161
- id: `subagent-${Date.now()}`,
162
- content: "",
163
- toolCalls: [],
164
- contentBlocks: [],
165
- isStreaming: true,
166
- };
167
- setMessages([currentMessageRef.current]);
168
- while (true) {
169
- const { done, value } = await reader.read();
170
- if (done) {
171
- logger.debug("Sub-agent SSE stream closed");
172
- break;
173
- }
174
- // Decode the chunk and add to buffer
175
- buffer += decoder.decode(value, { stream: true });
176
- // Process complete SSE messages
177
- const lines = buffer.split("\n");
178
- buffer = lines.pop() || ""; // Keep incomplete line in buffer
179
- let currentEvent = { event: "message", data: "" };
180
- for (const line of lines) {
181
- if (line.startsWith("event:")) {
182
- currentEvent.event = line.substring(6).trim();
183
- }
184
- else if (line.startsWith("data:")) {
185
- currentEvent.data = line.substring(5).trim();
186
- }
187
- else if (line === "") {
188
- // Empty line signals end of event
189
- if (currentEvent.event === "message" && currentEvent.data) {
190
- processSSEMessage(currentEvent.data);
191
- }
192
- // Reset for next event
193
- currentEvent = { event: "message", data: "" };
194
- }
195
- }
196
- }
197
- }
198
- catch (err) {
199
- if (err instanceof Error && err.name === "AbortError") {
200
- logger.debug("Sub-agent SSE stream aborted");
201
- }
202
- else {
203
- const errorMessage = err instanceof Error
204
- ? err.message
205
- : "Failed to connect to sub-agent";
206
- logger.error("Sub-agent SSE error", { error: errorMessage });
207
- setError(errorMessage);
208
- }
209
- }
210
- finally {
211
- // Mark streaming as complete
212
- if (currentMessageRef.current) {
213
- currentMessageRef.current.isStreaming = false;
214
- setMessages((prev) => prev.map((m) => m.id === currentMessageRef.current?.id
215
- ? { ...m, isStreaming: false }
216
- : m));
217
- }
218
- setHasCompleted(true);
219
- setIsStreaming(false);
220
- abortControllerRef.current = null;
221
- logger.debug("Sub-agent stream completed");
222
- }
223
- }, [processSSEMessage]);
224
- // Extract values from options (memoized to avoid dependency issues)
225
- const port = options?.port;
226
- const sessionId = options?.sessionId;
227
- const host = options?.host ??
228
- (typeof window !== "undefined" ? window.location.hostname : "localhost");
229
- const protocol = typeof window !== "undefined" ? window.location.protocol : "http:";
230
- // Connect when options change
231
- useEffect(() => {
232
- if (!port || !sessionId) {
233
- return;
234
- }
235
- // Reset state for new connection
236
- setMessages([]);
237
- setError(null);
238
- setHasCompleted(false);
239
- setIsStreaming(true);
240
- connectToSubagent(port, sessionId, host, protocol);
241
- // Cleanup on unmount or options change
242
- return () => {
243
- if (abortControllerRef.current) {
244
- abortControllerRef.current.abort();
245
- abortControllerRef.current = null;
246
- }
247
- if (updateTimeoutRef.current) {
248
- clearTimeout(updateTimeoutRef.current);
249
- updateTimeoutRef.current = null;
250
- }
251
- };
252
- }, [port, sessionId, host, protocol, connectToSubagent]);
253
- // Derive streaming status: streaming if we haven't completed yet
254
- const effectiveIsStreaming = !hasCompleted;
255
- return { messages, isStreaming: effectiveIsStreaming, error };
256
- }
@@ -1,9 +0,0 @@
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;
@@ -1,16 +0,0 @@
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
- }
@@ -1,8 +0,0 @@
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;
@@ -1,226 +0,0 @@
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
- const isSubagentCall = !!(toolCall.subagentPort && toolCall.subagentSessionId);
48
- // Safely access ChatLayout context - will be undefined if not within ChatLayout
49
- const layoutContext = React.useContext(ChatLayout.Context);
50
- // Click handler: toggle sidepanel for TodoWrite, subagent details for subagents, expand for others
51
- const handleHeaderClick = React.useCallback(() => {
52
- if (isTodoWrite && layoutContext) {
53
- // Toggle sidepanel - close if already open on todo tab, otherwise open
54
- if (layoutContext.panelSize !== "hidden" &&
55
- layoutContext.activeTab === "todo") {
56
- layoutContext.setPanelSize("hidden");
57
- }
58
- else {
59
- layoutContext.setPanelSize("small");
60
- layoutContext.setActiveTab("todo");
61
- }
62
- }
63
- else if (isSubagentCall) {
64
- // Toggle subagent details
65
- setIsSubagentExpanded(!isSubagentExpanded);
66
- }
67
- else {
68
- // Normal expand/collapse
69
- setIsExpanded(!isExpanded);
70
- }
71
- }, [
72
- isTodoWrite,
73
- layoutContext,
74
- isExpanded,
75
- isSubagentCall,
76
- isSubagentExpanded,
77
- ]);
78
- // Determine which icon to show
79
- const IconComponent = toolCall.icon && ICON_MAP[toolCall.icon]
80
- ? ICON_MAP[toolCall.icon]
81
- : CircleDot;
82
- // Determine display name
83
- const displayName = toolCall.prettyName || toolCall.title;
84
- // Determine icon color based on status (especially for subagents)
85
- const isSubagentRunning = isSubagentCall &&
86
- (toolCall.status === "in_progress" || toolCall.status === "pending");
87
- const isSubagentFailed = isSubagentCall && toolCall.status === "failed";
88
- const iconColorClass = isSubagentCall
89
- ? isSubagentFailed
90
- ? "text-destructive"
91
- : isSubagentRunning
92
- ? "text-foreground animate-pulse"
93
- : "text-green-500"
94
- : "text-muted-foreground";
95
- const statusTooltip = isSubagentCall
96
- ? isSubagentFailed
97
- ? "Sub-agent failed"
98
- : isSubagentRunning
99
- ? "Sub-agent running"
100
- : "Sub-agent completed"
101
- : undefined;
102
- // Check if there's an error
103
- const hasError = toolCall.status === "failed" || !!toolCall.error;
104
- // Check if this is a preliminary (pending) tool call without full details yet
105
- const isPreliminary = toolCall.status === "pending" &&
106
- (!toolCall.rawInput || Object.keys(toolCall.rawInput).length === 0);
107
- // JSON View style based on theme
108
- const jsonStyle = {
109
- fontSize: "11px",
110
- backgroundColor: "transparent",
111
- fontFamily: "inherit",
112
- "--w-rjv-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
113
- "--w-rjv-key-string": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
114
- "--w-rjv-background-color": "transparent",
115
- "--w-rjv-line-color": resolvedTheme === "dark" ? "#27272a" : "#e4e4e7",
116
- "--w-rjv-arrow-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
117
- "--w-rjv-edit-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
118
- "--w-rjv-info-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
119
- "--w-rjv-update-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
120
- "--w-rjv-copied-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
121
- "--w-rjv-copied-success-color": resolvedTheme === "dark" ? "#22c55e" : "#16a34a",
122
- "--w-rjv-curlybraces-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
123
- "--w-rjv-colon-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
124
- "--w-rjv-brackets-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
125
- "--w-rjv-quotes-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
126
- "--w-rjv-quotes-string-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
127
- "--w-rjv-type-string-color": resolvedTheme === "dark" ? "#22c55e" : "#16a34a",
128
- "--w-rjv-type-int-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
129
- "--w-rjv-type-float-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
130
- "--w-rjv-type-bigint-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
131
- "--w-rjv-type-boolean-color": resolvedTheme === "dark" ? "#3b82f6" : "#2563eb",
132
- "--w-rjv-type-date-color": resolvedTheme === "dark" ? "#ec4899" : "#db2777",
133
- "--w-rjv-type-url-color": resolvedTheme === "dark" ? "#3b82f6" : "#2563eb",
134
- "--w-rjv-type-null-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
135
- "--w-rjv-type-nan-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
136
- "--w-rjv-type-undefined-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
137
- };
138
- // Preliminary tool calls show as simple light gray text without expansion
139
- if (isPreliminary) {
140
- return (_jsx("div", { className: "flex flex-col my-4", children: _jsxs("span", { className: "text-paragraph-sm text-muted-foreground/50", children: ["Invoking ", displayName] }) }));
141
- }
142
- 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 }) })), !isTodoWrite && !isSubagentCall && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.rawInput &&
143
- Object.keys(toolCall.rawInput).length > 0 &&
144
- !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 &&
145
- loc.line !== undefined &&
146
- `:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), (toolCall.content && toolCall.content.length > 0) ||
147
- 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) => {
148
- // Generate a stable key based on content
149
- const getBlockKey = () => {
150
- if (block.type === "diff" && "path" in block) {
151
- return `diff-${block.path}-${idx}`;
152
- }
153
- if (block.type === "terminal" && "terminalId" in block) {
154
- return `terminal-${block.terminalId}`;
155
- }
156
- if (block.type === "text" && "text" in block) {
157
- return `text-${block.text.substring(0, 20)}-${idx}`;
158
- }
159
- if (block.type === "content" && "content" in block) {
160
- const innerContent = block.content;
161
- return `content-${innerContent.text?.substring(0, 20)}-${idx}`;
162
- }
163
- return `block-${idx}`;
164
- };
165
- // Helper to render text content (with JSON parsing if applicable)
166
- const renderTextContent = (text, key) => {
167
- // Try to parse as JSON
168
- try {
169
- const parsed = JSON.parse(text);
170
- // If it's an object or array, render with JsonView
171
- if (typeof parsed === "object" && parsed !== null) {
172
- return (_jsx("div", { className: "text-[11px]", children: _jsx(JsonView, { value: parsed, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) }, key));
173
- }
174
- }
175
- catch {
176
- // Not valid JSON, render as plain text
177
- }
178
- // Render as plain text
179
- return (_jsx("pre", { className: "whitespace-pre-wrap font-mono text-[11px] text-foreground overflow-x-auto", children: text }, key));
180
- };
181
- // Handle nested content blocks (ACP format)
182
- if (block.type === "content" && "content" in block) {
183
- const innerContent = block.content;
184
- if (innerContent.type === "text" && innerContent.text) {
185
- return renderTextContent(innerContent.text, getBlockKey());
186
- }
187
- }
188
- // Handle direct text blocks
189
- if (block.type === "text" && "text" in block) {
190
- return renderTextContent(block.text, getBlockKey());
191
- }
192
- // Handle image blocks
193
- if (block.type === "image") {
194
- const alt = block.alt || "Generated image";
195
- let imageSrc;
196
- if ("data" in block) {
197
- // Base64 encoded image
198
- const mimeType = block.mimeType || "image/png";
199
- imageSrc = `data:${mimeType};base64,${block.data}`;
200
- }
201
- else if ("url" in block) {
202
- // URL or file path
203
- imageSrc = block.url;
204
- }
205
- else {
206
- return null;
207
- }
208
- 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()));
209
- }
210
- // Handle diff blocks
211
- if (block.type === "diff" &&
212
- "path" in block &&
213
- "oldText" in block &&
214
- "newText" in block) {
215
- 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 &&
216
- block.line !== null &&
217
- block.line !== undefined &&
218
- `:${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()));
219
- }
220
- // Handle terminal blocks
221
- if (block.type === "terminal" && "terminalId" in block) {
222
- return (_jsxs("div", { className: "bg-neutral-900 text-neutral-100 p-2 rounded text-[11px] font-mono", children: ["Terminal: ", block.terminalId] }, getBlockKey()));
223
- }
224
- return null;
225
- }), 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)"] }))] }))] }))] }))] }));
226
- }
@@ -1,8 +0,0 @@
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;
@@ -1,29 +0,0 @@
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
- }