@townco/ui 0.1.110 → 0.1.112
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 +6 -0
- package/dist/core/hooks/use-subagent-stream.d.ts +28 -0
- package/dist/core/hooks/use-subagent-stream.js +256 -0
- package/dist/core/hooks/use-tool-calls.d.ts +12 -0
- package/dist/core/schemas/chat.d.ts +12 -0
- package/dist/core/schemas/tool-call.d.ts +18 -0
- package/dist/core/schemas/tool-call.js +8 -0
- package/dist/gui/components/InvokingGroup.d.ts +9 -0
- package/dist/gui/components/InvokingGroup.js +16 -0
- package/dist/gui/components/SubAgentDetails.js +7 -4
- package/dist/gui/components/ToolCall.d.ts +8 -0
- package/dist/gui/components/ToolCall.js +226 -0
- package/dist/gui/components/ToolCallGroup.d.ts +8 -0
- package/dist/gui/components/ToolCallGroup.js +29 -0
- package/dist/gui/components/ToolOperation.js +3 -1
- package/dist/sdk/schemas/session.d.ts +12 -0
- package/dist/sdk/transports/http.js +3 -29
- package/package.json +3 -3
|
@@ -158,6 +158,12 @@ export declare function useChatMessages(client: AcpClient | null, startSession:
|
|
|
158
158
|
};
|
|
159
159
|
})[] | undefined;
|
|
160
160
|
isStreaming?: boolean | undefined;
|
|
161
|
+
_meta?: {
|
|
162
|
+
semanticName?: string | undefined;
|
|
163
|
+
agentDefinitionName?: string | undefined;
|
|
164
|
+
currentActivity?: string | undefined;
|
|
165
|
+
statusGenerating?: boolean | undefined;
|
|
166
|
+
} | undefined;
|
|
161
167
|
}[] | undefined;
|
|
162
168
|
subagentStreaming?: boolean | undefined;
|
|
163
169
|
subagentCompleted?: boolean | undefined;
|
|
@@ -0,0 +1,28 @@
|
|
|
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;
|
|
@@ -0,0 +1,256 @@
|
|
|
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
|
+
}
|
|
@@ -155,6 +155,12 @@ export declare function useToolCalls(client: AcpClient | null): {
|
|
|
155
155
|
};
|
|
156
156
|
})[] | undefined;
|
|
157
157
|
isStreaming?: boolean | undefined;
|
|
158
|
+
_meta?: {
|
|
159
|
+
semanticName?: string | undefined;
|
|
160
|
+
agentDefinitionName?: string | undefined;
|
|
161
|
+
currentActivity?: string | undefined;
|
|
162
|
+
statusGenerating?: boolean | undefined;
|
|
163
|
+
} | undefined;
|
|
158
164
|
}[] | undefined;
|
|
159
165
|
subagentStreaming?: boolean | undefined;
|
|
160
166
|
subagentCompleted?: boolean | undefined;
|
|
@@ -306,6 +312,12 @@ export declare function useToolCalls(client: AcpClient | null): {
|
|
|
306
312
|
};
|
|
307
313
|
})[] | undefined;
|
|
308
314
|
isStreaming?: boolean | undefined;
|
|
315
|
+
_meta?: {
|
|
316
|
+
semanticName?: string | undefined;
|
|
317
|
+
agentDefinitionName?: string | undefined;
|
|
318
|
+
currentActivity?: string | undefined;
|
|
319
|
+
statusGenerating?: boolean | undefined;
|
|
320
|
+
} | undefined;
|
|
309
321
|
}[] | undefined;
|
|
310
322
|
subagentStreaming?: boolean | undefined;
|
|
311
323
|
subagentCompleted?: boolean | undefined;
|
|
@@ -237,6 +237,12 @@ export declare const DisplayMessage: z.ZodObject<{
|
|
|
237
237
|
}, z.core.$strip>;
|
|
238
238
|
}, z.core.$strip>], "type">>>;
|
|
239
239
|
isStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
240
|
+
_meta: z.ZodOptional<z.ZodObject<{
|
|
241
|
+
semanticName: z.ZodOptional<z.ZodString>;
|
|
242
|
+
agentDefinitionName: z.ZodOptional<z.ZodString>;
|
|
243
|
+
currentActivity: z.ZodOptional<z.ZodString>;
|
|
244
|
+
statusGenerating: z.ZodOptional<z.ZodBoolean>;
|
|
245
|
+
}, z.core.$strip>>;
|
|
240
246
|
}, z.core.$strip>>>;
|
|
241
247
|
subagentStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
242
248
|
subagentCompleted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -502,6 +508,12 @@ export declare const ChatSessionState: z.ZodObject<{
|
|
|
502
508
|
}, z.core.$strip>;
|
|
503
509
|
}, z.core.$strip>], "type">>>;
|
|
504
510
|
isStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
511
|
+
_meta: z.ZodOptional<z.ZodObject<{
|
|
512
|
+
semanticName: z.ZodOptional<z.ZodString>;
|
|
513
|
+
agentDefinitionName: z.ZodOptional<z.ZodString>;
|
|
514
|
+
currentActivity: z.ZodOptional<z.ZodString>;
|
|
515
|
+
statusGenerating: z.ZodOptional<z.ZodBoolean>;
|
|
516
|
+
}, z.core.$strip>>;
|
|
505
517
|
}, z.core.$strip>>>;
|
|
506
518
|
subagentStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
507
519
|
subagentCompleted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -262,6 +262,12 @@ export declare const SubagentMessageSchema: z.ZodObject<{
|
|
|
262
262
|
}, z.core.$strip>;
|
|
263
263
|
}, z.core.$strip>], "type">>>;
|
|
264
264
|
isStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
265
|
+
_meta: z.ZodOptional<z.ZodObject<{
|
|
266
|
+
semanticName: z.ZodOptional<z.ZodString>;
|
|
267
|
+
agentDefinitionName: z.ZodOptional<z.ZodString>;
|
|
268
|
+
currentActivity: z.ZodOptional<z.ZodString>;
|
|
269
|
+
statusGenerating: z.ZodOptional<z.ZodBoolean>;
|
|
270
|
+
}, z.core.$strip>>;
|
|
265
271
|
}, z.core.$strip>;
|
|
266
272
|
export type SubagentMessage = z.infer<typeof SubagentMessageSchema>;
|
|
267
273
|
/**
|
|
@@ -443,6 +449,12 @@ export declare const ToolCallSchema: z.ZodObject<{
|
|
|
443
449
|
}, z.core.$strip>;
|
|
444
450
|
}, z.core.$strip>], "type">>>;
|
|
445
451
|
isStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
452
|
+
_meta: z.ZodOptional<z.ZodObject<{
|
|
453
|
+
semanticName: z.ZodOptional<z.ZodString>;
|
|
454
|
+
agentDefinitionName: z.ZodOptional<z.ZodString>;
|
|
455
|
+
currentActivity: z.ZodOptional<z.ZodString>;
|
|
456
|
+
statusGenerating: z.ZodOptional<z.ZodBoolean>;
|
|
457
|
+
}, z.core.$strip>>;
|
|
446
458
|
}, z.core.$strip>>>;
|
|
447
459
|
subagentStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
448
460
|
subagentCompleted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -596,6 +608,12 @@ export declare const ToolCallUpdateSchema: z.ZodObject<{
|
|
|
596
608
|
}, z.core.$strip>;
|
|
597
609
|
}, z.core.$strip>], "type">>>;
|
|
598
610
|
isStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
611
|
+
_meta: z.ZodOptional<z.ZodObject<{
|
|
612
|
+
semanticName: z.ZodOptional<z.ZodString>;
|
|
613
|
+
agentDefinitionName: z.ZodOptional<z.ZodString>;
|
|
614
|
+
currentActivity: z.ZodOptional<z.ZodString>;
|
|
615
|
+
statusGenerating: z.ZodOptional<z.ZodBoolean>;
|
|
616
|
+
}, z.core.$strip>>;
|
|
599
617
|
}, z.core.$strip>>>;
|
|
600
618
|
subagentCompleted: z.ZodOptional<z.ZodBoolean>;
|
|
601
619
|
_meta: z.ZodOptional<z.ZodObject<{
|
|
@@ -117,6 +117,14 @@ export const SubagentMessageSchema = z.object({
|
|
|
117
117
|
/** Interleaved content blocks in arrival order */
|
|
118
118
|
contentBlocks: z.array(SubagentContentBlockSchema).optional(),
|
|
119
119
|
isStreaming: z.boolean().optional(),
|
|
120
|
+
_meta: z
|
|
121
|
+
.object({
|
|
122
|
+
semanticName: z.string().optional(),
|
|
123
|
+
agentDefinitionName: z.string().optional(),
|
|
124
|
+
currentActivity: z.string().optional(),
|
|
125
|
+
statusGenerating: z.boolean().optional(),
|
|
126
|
+
})
|
|
127
|
+
.optional(),
|
|
120
128
|
});
|
|
121
129
|
/**
|
|
122
130
|
* Complete tool call state as displayed in the UI
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
|
|
2
|
+
export interface InvokingGroupProps {
|
|
3
|
+
toolCalls: ToolCallType[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* InvokingGroup component - displays a group of preliminary (invoking) tool calls
|
|
7
|
+
* Shows as "Invoking parallel operation (N)" with a summary of unique tool names
|
|
8
|
+
*/
|
|
9
|
+
export declare function InvokingGroup({ toolCalls }: InvokingGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ListVideo } from "lucide-react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
/**
|
|
5
|
+
* InvokingGroup component - displays a group of preliminary (invoking) tool calls
|
|
6
|
+
* Shows as "Invoking parallel operation (N)" with a summary of unique tool names
|
|
7
|
+
*/
|
|
8
|
+
export function InvokingGroup({ toolCalls }) {
|
|
9
|
+
// Get unique display names for the summary
|
|
10
|
+
const displayNames = toolCalls.map((tc) => tc.prettyName || tc.title);
|
|
11
|
+
const uniqueNames = [...new Set(displayNames)];
|
|
12
|
+
const summary = uniqueNames.length <= 2
|
|
13
|
+
? uniqueNames.join(", ")
|
|
14
|
+
: `${uniqueNames.slice(0, 2).join(", ")} +${uniqueNames.length - 2} more`;
|
|
15
|
+
return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-paragraph-sm text-muted-foreground/50", children: [_jsx(ListVideo, { className: "h-3 w-3" }), _jsx("span", { children: "Invoking parallel operation" }), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-muted-foreground/50", children: toolCalls.length })] }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground/50 pl-4.5", children: summary })] }));
|
|
16
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronDown, Loader2 } from "lucide-react";
|
|
3
3
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
4
4
|
import { MarkdownRenderer } from "./MarkdownRenderer.js";
|
|
@@ -93,10 +93,13 @@ export function SubAgentDetails({ parentStatus, agentName, query, isExpanded: co
|
|
|
93
93
|
? (query.split("\n")[0] ?? "").slice(0, 100) +
|
|
94
94
|
(query.length > 100 ? "..." : "")
|
|
95
95
|
: "";
|
|
96
|
-
return (_jsxs("div", { children: [!isExpanded && (_jsx("div", { className: "w-full max-w-md", children: previewText ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/70 truncate", children: previewText })) : queryFirstLine ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 truncate", children: queryFirstLine })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [
|
|
96
|
+
return (_jsxs("div", { children: [!isExpanded && (_jsx("div", { className: "w-full max-w-md", children: currentMessage?._meta?.currentActivity ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/70 truncate", children: currentMessage._meta.currentActivity })) : currentMessage?._meta?.statusGenerating ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 truncate animate-pulse", children: "Determining status..." })) : previewText ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/70 truncate", children: previewText })) : queryFirstLine ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 truncate", children: queryFirstLine })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [
|
|
97
97
|
_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsxs("div", { className: "text-[11px] font-mono space-y-1", children: [agentName && (_jsxs("div", { children: [
|
|
98
|
-
_jsx("span", { className: "text-muted-foreground", children:
|
|
99
|
-
|
|
98
|
+
_jsx("span", { className: "text-muted-foreground", children: storedMessages?.[0]?._meta?.semanticName
|
|
99
|
+
? "Task: "
|
|
100
|
+
: "agentName: " }), storedMessages?.[0]?._meta?.semanticName ? (_jsxs(_Fragment, { children: [
|
|
101
|
+
_jsx("span", { className: "text-foreground font-medium", children: storedMessages[0]._meta.semanticName }), _jsxs("span", { className: "text-muted-foreground text-[10px] ml-1", children: ["(", agentName, ")"] })
|
|
102
|
+
] })) : (_jsx("span", { className: "text-foreground", children: agentName }))] })), query && (_jsxs("div", { children: [
|
|
100
103
|
_jsx("span", { className: "text-muted-foreground", children: "query: " }), _jsx("span", { className: "text-foreground", children: query })
|
|
101
104
|
] }))] })
|
|
102
105
|
] })), _jsxs("div", { children: [
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
|
|
2
|
+
export interface ToolCallProps {
|
|
3
|
+
toolCall: ToolCallType;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* ToolCall component - displays a single tool call with collapsible details
|
|
7
|
+
*/
|
|
8
|
+
export declare function ToolCall({ toolCall }: ToolCallProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,226 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
|
|
2
|
+
export interface ToolCallGroupProps {
|
|
3
|
+
toolCalls: ToolCallType[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* ToolCallGroup component - displays a group of parallel tool calls with collapsible details
|
|
7
|
+
*/
|
|
8
|
+
export declare function ToolCallGroup({ toolCalls }: ToolCallGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, ListVideo } from "lucide-react";
|
|
3
|
+
import React, { useState } from "react";
|
|
4
|
+
import { ToolCall } from "./ToolCall.js";
|
|
5
|
+
/**
|
|
6
|
+
* ToolCallGroup component - displays a group of parallel tool calls with collapsible details
|
|
7
|
+
*/
|
|
8
|
+
export function ToolCallGroup({ toolCalls }) {
|
|
9
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
10
|
+
// Calculate group status based on individual tool call statuses
|
|
11
|
+
const getGroupStatus = () => {
|
|
12
|
+
const statuses = toolCalls.map((tc) => tc.status);
|
|
13
|
+
if (statuses.some((s) => s === "failed"))
|
|
14
|
+
return "failed";
|
|
15
|
+
if (statuses.some((s) => s === "in_progress"))
|
|
16
|
+
return "in_progress";
|
|
17
|
+
if (statuses.every((s) => s === "completed"))
|
|
18
|
+
return "completed";
|
|
19
|
+
return "pending";
|
|
20
|
+
};
|
|
21
|
+
const groupStatus = getGroupStatus();
|
|
22
|
+
// Generate summary of tool names
|
|
23
|
+
const toolNames = toolCalls.map((tc) => tc.prettyName || tc.title);
|
|
24
|
+
const uniqueNames = [...new Set(toolNames)];
|
|
25
|
+
const summary = uniqueNames.length <= 2
|
|
26
|
+
? uniqueNames.join(", ")
|
|
27
|
+
: `${uniqueNames.slice(0, 2).join(", ")} +${uniqueNames.length - 2} more`;
|
|
28
|
+
return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: "text-muted-foreground", children: _jsx(ListVideo, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: "Parallel operation" }), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-muted-foreground/70", children: toolCalls.length }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` })] }), !isExpanded && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: summary }))] }), isExpanded && (_jsx("div", { className: "mt-1", children: toolCalls.map((toolCall) => (_jsxs("div", { className: "flex items-start", children: [_jsx("div", { className: "w-2.5 h-4 border-l-2 border-b-2 border-border rounded-bl-[6px] mt-1 mr-0.5 shrink-0" }), _jsx("div", { className: "flex-1 -mt-2", children: _jsx(ToolCall, { toolCall: toolCall }) })] }, toolCall.id))) }))] }));
|
|
29
|
+
}
|
|
@@ -349,7 +349,9 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
|
|
|
349
349
|
// Render subagent with clickable header and SubAgentDetails component
|
|
350
350
|
return (_jsxs("div", { className: "flex flex-col ml-5", children: [
|
|
351
351
|
_jsxs("button", { type: "button", className: "flex items-center gap-1.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: () => setIsExpanded(!isExpanded), "aria-expanded": isExpanded, children: [
|
|
352
|
-
_jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(CircleDot, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.
|
|
352
|
+
_jsx("div", { className: "text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: _jsx(CircleDot, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-text-secondary/70 group-hover:text-text-secondary transition-colors", children: toolCall.subagentMessages?.[0]?._meta?.semanticName ||
|
|
353
|
+
toolCall.rawInput?.agentName ||
|
|
354
|
+
"Subagent" }), toolCall.subagentMessages?.[0]?._meta?.semanticName && (_jsxs("span", { className: "text-muted-foreground text-[10px] ml-1", children: ["(", toolCall.rawInput?.agentName || "subagent", ")"] })), isRunning && toolCall.startedAt && (_jsx(RunningDuration, { startTime: toolCall.startedAt })), isFailed && (_jsx("span", { title: toolCall.error || "Operation failed", children: _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }) })), hasCompaction && (_jsx(TooltipProvider, { delayDuration: 0, children: _jsxs(Tooltip, { children: [
|
|
353
355
|
_jsx(TooltipTrigger, { asChild: true, children: _jsx("span", { children: isTruncation ? (_jsx(ScissorsLineDashed, { className: "h-3 w-3 text-destructive" })) : (_jsx(FoldVertical, { className: "h-3 w-3 text-text-secondary/70" })) }) }), _jsx(TooltipContent, { children: (() => {
|
|
354
356
|
const meta = toolCall._meta;
|
|
355
357
|
const percentage = meta?.originalTokens && meta?.finalTokens
|
|
@@ -348,6 +348,12 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
348
348
|
}, z.core.$strip>;
|
|
349
349
|
}, z.core.$strip>], "type">>>;
|
|
350
350
|
isStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
351
|
+
_meta: z.ZodOptional<z.ZodObject<{
|
|
352
|
+
semanticName: z.ZodOptional<z.ZodString>;
|
|
353
|
+
agentDefinitionName: z.ZodOptional<z.ZodString>;
|
|
354
|
+
currentActivity: z.ZodOptional<z.ZodString>;
|
|
355
|
+
statusGenerating: z.ZodOptional<z.ZodBoolean>;
|
|
356
|
+
}, z.core.$strip>>;
|
|
351
357
|
}, z.core.$strip>>>;
|
|
352
358
|
subagentStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
353
359
|
subagentCompleted: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -557,6 +563,12 @@ export declare const SessionUpdate: z.ZodUnion<readonly [z.ZodObject<{
|
|
|
557
563
|
}, z.core.$strip>;
|
|
558
564
|
}, z.core.$strip>], "type">>>;
|
|
559
565
|
isStreaming: z.ZodOptional<z.ZodBoolean>;
|
|
566
|
+
_meta: z.ZodOptional<z.ZodObject<{
|
|
567
|
+
semanticName: z.ZodOptional<z.ZodString>;
|
|
568
|
+
agentDefinitionName: z.ZodOptional<z.ZodString>;
|
|
569
|
+
currentActivity: z.ZodOptional<z.ZodString>;
|
|
570
|
+
statusGenerating: z.ZodOptional<z.ZodBoolean>;
|
|
571
|
+
}, z.core.$strip>>;
|
|
560
572
|
}, z.core.$strip>>>;
|
|
561
573
|
subagentCompleted: z.ZodOptional<z.ZodBoolean>;
|
|
562
574
|
_meta: z.ZodOptional<z.ZodObject<{
|
|
@@ -872,19 +872,14 @@ export class HttpTransport {
|
|
|
872
872
|
message.method === "session/update" &&
|
|
873
873
|
message.params?.update?.sessionUpdate === "sources";
|
|
874
874
|
if (isSourcesMessage) {
|
|
875
|
-
// Use console.warn directly for gui-console capture
|
|
876
|
-
console.warn("🟢 RECEIVED SOURCES SSE MESSAGE", {
|
|
877
|
-
sourcesCount: message.params?.update?.sources?.length,
|
|
878
|
-
isInReplayMode: this.isInReplayMode,
|
|
879
|
-
callbackCount: this.sessionUpdateCallbacks.size,
|
|
880
|
-
});
|
|
881
875
|
// Handle sources directly without ACP schema validation
|
|
882
876
|
try {
|
|
883
877
|
this.handleSessionNotification(message.params);
|
|
884
|
-
console.warn("🟢 AFTER handleSessionNotification for sources");
|
|
885
878
|
}
|
|
886
879
|
catch (error) {
|
|
887
|
-
|
|
880
|
+
logger.error("Error in handleSessionNotification for sources", {
|
|
881
|
+
error,
|
|
882
|
+
});
|
|
888
883
|
}
|
|
889
884
|
return;
|
|
890
885
|
}
|
|
@@ -931,9 +926,6 @@ export class HttpTransport {
|
|
|
931
926
|
logger.debug("Skipping session notification - stream complete/cancelled");
|
|
932
927
|
return;
|
|
933
928
|
}
|
|
934
|
-
if (this.streamComplete && isSourcesNotification) {
|
|
935
|
-
console.warn("🟢 Processing sources notification after stream complete");
|
|
936
|
-
}
|
|
937
929
|
logger.debug("handleSessionNotification called", { params });
|
|
938
930
|
// Extract content from the update
|
|
939
931
|
const paramsExtended = params;
|
|
@@ -1548,13 +1540,6 @@ export class HttpTransport {
|
|
|
1548
1540
|
update.sessionUpdate === "sources") {
|
|
1549
1541
|
// Sources notification - citation sources from tool calls
|
|
1550
1542
|
const sourcesUpdate = update;
|
|
1551
|
-
console.warn("🔵 SOURCES in handleSessionNotification", {
|
|
1552
|
-
sourcesCount: sourcesUpdate.sources.length,
|
|
1553
|
-
isInReplayMode: this.isInReplayMode,
|
|
1554
|
-
callbackCount: this.sessionUpdateCallbacks.size,
|
|
1555
|
-
firstSourceId: sourcesUpdate.sources[0]?.id,
|
|
1556
|
-
firstSourceToolCallId: sourcesUpdate.sources[0]?.toolCallId,
|
|
1557
|
-
});
|
|
1558
1543
|
// Create a sources session update
|
|
1559
1544
|
const sessionUpdate = {
|
|
1560
1545
|
type: "sources",
|
|
@@ -1567,20 +1552,9 @@ export class HttpTransport {
|
|
|
1567
1552
|
// If no callbacks are registered yet (React hooks haven't subscribed),
|
|
1568
1553
|
// queue the sources to be replayed when they do subscribe
|
|
1569
1554
|
if (this.sessionUpdateCallbacks.size === 0) {
|
|
1570
|
-
console.warn("🔵 QUEUEING sources for late-subscribing callbacks", {
|
|
1571
|
-
sourcesCount: sourcesUpdate.sources.length,
|
|
1572
|
-
queueLengthBefore: this.pendingReplayUpdates.length,
|
|
1573
|
-
});
|
|
1574
1555
|
this.pendingReplayUpdates.push(sessionUpdate);
|
|
1575
|
-
console.warn("🔵 Queue length after:", {
|
|
1576
|
-
queueLengthAfter: this.pendingReplayUpdates.length,
|
|
1577
|
-
});
|
|
1578
1556
|
}
|
|
1579
1557
|
else {
|
|
1580
|
-
console.warn("🔵 NOTIFYING sources immediately (callbacks registered)", {
|
|
1581
|
-
sourcesCount: sourcesUpdate.sources.length,
|
|
1582
|
-
callbackCount: this.sessionUpdateCallbacks.size,
|
|
1583
|
-
});
|
|
1584
1558
|
this.notifySessionUpdate(sessionUpdate);
|
|
1585
1559
|
}
|
|
1586
1560
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.112",
|
|
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.90",
|
|
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,7 +67,7 @@
|
|
|
67
67
|
"zustand": "^5.0.8"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
|
-
"@townco/tsconfig": "0.1.
|
|
70
|
+
"@townco/tsconfig": "0.1.109",
|
|
71
71
|
"@types/node": "^24.10.0",
|
|
72
72
|
"@types/react": "^19.2.2",
|
|
73
73
|
"@types/unist": "^3.0.3",
|