@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.
- package/dist/core/hooks/use-chat-messages.d.ts +1 -0
- package/dist/core/hooks/use-chat-session.js +18 -2
- package/dist/core/hooks/use-tool-calls.d.ts +2 -0
- package/dist/core/hooks/use-tool-calls.js +0 -12
- package/dist/core/schemas/chat.d.ts +2 -0
- package/dist/core/schemas/tool-call.d.ts +2 -0
- package/dist/core/schemas/tool-call.js +6 -0
- package/dist/core/store/chat-store.js +6 -3
- package/dist/core/utils/tool-call-state.js +8 -1
- package/dist/gui/components/MessageContent.js +20 -4
- package/dist/gui/components/SubAgentDetails.js +0 -19
- package/dist/gui/components/ToolOperation.js +13 -7
- package/dist/sdk/schemas/session.d.ts +2 -0
- package/dist/sdk/transports/http.js +15 -2
- package/package.json +6 -6
- package/dist/core/hooks/use-subagent-stream.d.ts +0 -28
- package/dist/core/hooks/use-subagent-stream.js +0 -256
- package/dist/gui/components/InvokingGroup.d.ts +0 -9
- package/dist/gui/components/InvokingGroup.js +0 -16
- package/dist/gui/components/ToolCall.d.ts +0 -8
- package/dist/gui/components/ToolCall.js +0 -226
- package/dist/gui/components/ToolCallGroup.d.ts +0 -8
- package/dist/gui/components/ToolCallGroup.js +0 -29
|
@@ -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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
//
|
|
1496
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
82
|
-
"react-dom": "^19.2.
|
|
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
|
-
}
|