@townco/ui 0.1.104 → 0.1.106
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/browser-logger.d.ts +10 -0
- package/dist/browser-logger.js +141 -0
- package/dist/core/hooks/index.d.ts +0 -1
- package/dist/core/hooks/index.js +0 -1
- package/dist/core/hooks/use-tool-calls.js +11 -0
- package/dist/gui/components/ChatView.js +10 -0
- package/dist/gui/components/SubAgentDetails.d.ts +3 -12
- package/dist/gui/components/SubAgentDetails.js +27 -31
- package/dist/gui/components/ToolOperation.js +6 -12
- 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
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser console capture and server sync
|
|
3
|
+
* Intercepts console methods and sends logs to the agent server
|
|
4
|
+
* Follows the pattern from packages/ui/src/sdk/transports/stdio.ts
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Initialize browser logger
|
|
8
|
+
* Must be called early, before other code that might log
|
|
9
|
+
*/
|
|
10
|
+
export declare function initBrowserLogger(agentServerUrl: string): void;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser console capture and server sync
|
|
3
|
+
* Intercepts console methods and sends logs to the agent server
|
|
4
|
+
* Follows the pattern from packages/ui/src/sdk/transports/stdio.ts
|
|
5
|
+
*/
|
|
6
|
+
import { createLogger, subscribeToLogs } from "@townco/core";
|
|
7
|
+
let initialized = false;
|
|
8
|
+
let serverUrl = null;
|
|
9
|
+
const logQueue = [];
|
|
10
|
+
let flushTimeoutId = null;
|
|
11
|
+
// Store original console methods to preserve DevTools output
|
|
12
|
+
const originalConsole = {
|
|
13
|
+
log: console.log.bind(console),
|
|
14
|
+
error: console.error.bind(console),
|
|
15
|
+
warn: console.warn.bind(console),
|
|
16
|
+
info: console.info.bind(console),
|
|
17
|
+
debug: console.debug.bind(console),
|
|
18
|
+
};
|
|
19
|
+
const logger = createLogger("gui-console");
|
|
20
|
+
/**
|
|
21
|
+
* Safely stringify arguments for logging
|
|
22
|
+
*/
|
|
23
|
+
function stringifyArgs(args) {
|
|
24
|
+
return args
|
|
25
|
+
.map((arg) => {
|
|
26
|
+
if (typeof arg === "string")
|
|
27
|
+
return arg;
|
|
28
|
+
if (arg instanceof Error)
|
|
29
|
+
return `${arg.name}: ${arg.message}`;
|
|
30
|
+
try {
|
|
31
|
+
return JSON.stringify(arg);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return String(arg);
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
.join(" ");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Queue a log entry to be sent to the server
|
|
41
|
+
*/
|
|
42
|
+
function queueLog(entry) {
|
|
43
|
+
logQueue.push(entry);
|
|
44
|
+
// Immediate flush for errors
|
|
45
|
+
if (entry.level === "error" || entry.level === "fatal") {
|
|
46
|
+
flushLogs();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Debounced flush for other levels
|
|
50
|
+
if (!flushTimeoutId) {
|
|
51
|
+
flushTimeoutId = setTimeout(() => {
|
|
52
|
+
flushTimeoutId = null;
|
|
53
|
+
flushLogs();
|
|
54
|
+
}, 500);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Send queued logs to the server
|
|
59
|
+
*/
|
|
60
|
+
async function flushLogs() {
|
|
61
|
+
if (logQueue.length === 0 || !serverUrl)
|
|
62
|
+
return;
|
|
63
|
+
const logs = logQueue.splice(0);
|
|
64
|
+
try {
|
|
65
|
+
await fetch(`${serverUrl}/logs/browser`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/json" },
|
|
68
|
+
body: JSON.stringify({ logs }),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Silent fail - don't break the app if server is unavailable
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Initialize browser logger
|
|
77
|
+
* Must be called early, before other code that might log
|
|
78
|
+
*/
|
|
79
|
+
export function initBrowserLogger(agentServerUrl) {
|
|
80
|
+
if (initialized)
|
|
81
|
+
return;
|
|
82
|
+
initialized = true;
|
|
83
|
+
serverUrl = agentServerUrl;
|
|
84
|
+
// Override console methods - preserve original output, then log
|
|
85
|
+
console.log = (...args) => {
|
|
86
|
+
originalConsole.log(...args);
|
|
87
|
+
logger.info(stringifyArgs(args));
|
|
88
|
+
};
|
|
89
|
+
console.info = (...args) => {
|
|
90
|
+
originalConsole.info(...args);
|
|
91
|
+
logger.info(stringifyArgs(args));
|
|
92
|
+
};
|
|
93
|
+
console.warn = (...args) => {
|
|
94
|
+
originalConsole.warn(...args);
|
|
95
|
+
logger.warn(stringifyArgs(args));
|
|
96
|
+
};
|
|
97
|
+
console.error = (...args) => {
|
|
98
|
+
originalConsole.error(...args);
|
|
99
|
+
logger.error(stringifyArgs(args));
|
|
100
|
+
};
|
|
101
|
+
console.debug = (...args) => {
|
|
102
|
+
originalConsole.debug(...args);
|
|
103
|
+
logger.debug(stringifyArgs(args));
|
|
104
|
+
};
|
|
105
|
+
// Capture uncaught exceptions
|
|
106
|
+
window.onerror = (message, source, line, col, error) => {
|
|
107
|
+
logger.error(String(message), {
|
|
108
|
+
source,
|
|
109
|
+
line,
|
|
110
|
+
col,
|
|
111
|
+
stack: error?.stack,
|
|
112
|
+
});
|
|
113
|
+
return false; // Let default handler run too
|
|
114
|
+
};
|
|
115
|
+
// Capture unhandled promise rejections
|
|
116
|
+
window.onunhandledrejection = (event) => {
|
|
117
|
+
const reason = event.reason instanceof Error
|
|
118
|
+
? `${event.reason.name}: ${event.reason.message}`
|
|
119
|
+
: String(event.reason);
|
|
120
|
+
logger.error(`Unhandled rejection: ${reason}`, {
|
|
121
|
+
stack: event.reason?.stack,
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
// Subscribe to logs and queue for server sync
|
|
125
|
+
subscribeToLogs((entry) => {
|
|
126
|
+
// Only sync gui-console logs to avoid duplicates
|
|
127
|
+
if (entry.service === "gui-console") {
|
|
128
|
+
queueLog(entry);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
// Flush logs before page unload
|
|
132
|
+
window.addEventListener("beforeunload", () => {
|
|
133
|
+
if (flushTimeoutId) {
|
|
134
|
+
clearTimeout(flushTimeoutId);
|
|
135
|
+
}
|
|
136
|
+
// Use sendBeacon for reliable delivery on page unload
|
|
137
|
+
if (logQueue.length > 0 && serverUrl) {
|
|
138
|
+
navigator.sendBeacon(`${serverUrl}/logs/browser`, JSON.stringify({ logs: logQueue }));
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
package/dist/core/hooks/index.js
CHANGED
|
@@ -29,10 +29,21 @@ export function useToolCalls(client) {
|
|
|
29
29
|
}
|
|
30
30
|
else if (update.type === "tool_call_update") {
|
|
31
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
|
+
});
|
|
32
39
|
// Update session-level tool calls (for sidebar)
|
|
33
40
|
updateToolCall(update.sessionId, update.toolCallUpdate);
|
|
34
41
|
// Also update in current assistant message (for inline display)
|
|
35
42
|
updateToolCallInCurrentMessage(update.toolCallUpdate);
|
|
43
|
+
_logger.info("[SUBAGENT] Successfully updated tool call state", {
|
|
44
|
+
sessionId: update.sessionId,
|
|
45
|
+
toolCallId: update.toolCallUpdate.id,
|
|
46
|
+
});
|
|
36
47
|
}
|
|
37
48
|
});
|
|
38
49
|
return () => {
|
|
@@ -3,6 +3,7 @@ import { createLogger } from "@townco/core";
|
|
|
3
3
|
import { ArrowUp, Bug, Moon, PanelRight, Settings, Sun, X } from "lucide-react";
|
|
4
4
|
import * as React from "react";
|
|
5
5
|
import { useEffect, useState } from "react";
|
|
6
|
+
import { initBrowserLogger } from "../../browser-logger.js";
|
|
6
7
|
import { useChatMessages, useChatSession, useToolCalls, } from "../../core/hooks/index.js";
|
|
7
8
|
import { selectTodosForCurrentSession, useChatStore, } from "../../core/store/chat-store.js";
|
|
8
9
|
import { useDocumentTitle } from "../hooks/use-favicon.js";
|
|
@@ -119,6 +120,15 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
|
|
|
119
120
|
}, [messages]);
|
|
120
121
|
// Update document title with agent name and animated dots when streaming
|
|
121
122
|
useDocumentTitle(agentName);
|
|
123
|
+
// Initialize browser logger to capture console output and send to server
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (client) {
|
|
126
|
+
const baseUrl = client.getBaseUrl();
|
|
127
|
+
if (baseUrl) {
|
|
128
|
+
initBrowserLogger(baseUrl);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, [client]);
|
|
122
132
|
// Log connection status changes
|
|
123
133
|
useEffect(() => {
|
|
124
134
|
logger.debug("Connection status changed", { status: connectionStatus });
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
|
|
2
2
|
export interface SubAgentDetailsProps {
|
|
3
|
-
/** Sub-agent HTTP port (required for live streaming, not for replay) */
|
|
4
|
-
port?: number | undefined;
|
|
5
|
-
/** Sub-agent session ID (required for live streaming, not for replay) */
|
|
6
|
-
sessionId?: string | undefined;
|
|
7
|
-
/** Optional host (defaults to localhost) */
|
|
8
|
-
host?: string;
|
|
9
3
|
/** Parent tool call status - use this to determine if sub-agent is running */
|
|
10
4
|
parentStatus?: "pending" | "in_progress" | "completed" | "failed";
|
|
11
5
|
/** Sub-agent name (for display) */
|
|
@@ -16,19 +10,16 @@ export interface SubAgentDetailsProps {
|
|
|
16
10
|
isExpanded?: boolean;
|
|
17
11
|
/** Callback when expand state changes */
|
|
18
12
|
onExpandChange?: (expanded: boolean) => void;
|
|
19
|
-
/**
|
|
13
|
+
/** Messages from the subagent (streamed via parent tool_call_update) */
|
|
20
14
|
storedMessages?: ToolCallType["subagentMessages"];
|
|
21
|
-
/** Whether this is a replay (skips SSE connection) */
|
|
22
|
-
isReplay?: boolean | undefined;
|
|
23
15
|
}
|
|
24
16
|
/**
|
|
25
17
|
* SubAgentDetails component - displays streaming content from a sub-agent.
|
|
26
18
|
*
|
|
27
19
|
* This component:
|
|
28
|
-
* -
|
|
29
|
-
* - Or displays stored messages (replay mode)
|
|
20
|
+
* - Displays messages from the subagent (received via parent tool_call_update events)
|
|
30
21
|
* - Displays streaming text and tool calls
|
|
31
22
|
* - Renders content as markdown
|
|
32
23
|
* - Renders in a collapsible section (collapsed by default)
|
|
33
24
|
*/
|
|
34
|
-
export declare function SubAgentDetails({
|
|
25
|
+
export declare function SubAgentDetails({ parentStatus, agentName, query, isExpanded: controlledIsExpanded, onExpandChange, storedMessages }: SubAgentDetailsProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ChevronDown, Loader2 } from "lucide-react";
|
|
3
|
-
import { useCallback, useEffect,
|
|
4
|
-
import { useSubagentStream } from "../../core/hooks/use-subagent-stream.js";
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
4
|
import { MarkdownRenderer } from "./MarkdownRenderer.js";
|
|
6
5
|
const SCROLL_THRESHOLD = 50; // px from bottom to consider "at bottom"
|
|
7
6
|
/**
|
|
8
7
|
* SubAgentDetails component - displays streaming content from a sub-agent.
|
|
9
8
|
*
|
|
10
9
|
* This component:
|
|
11
|
-
* -
|
|
12
|
-
* - Or displays stored messages (replay mode)
|
|
10
|
+
* - Displays messages from the subagent (received via parent tool_call_update events)
|
|
13
11
|
* - Displays streaming text and tool calls
|
|
14
12
|
* - Renders content as markdown
|
|
15
13
|
* - Renders in a collapsible section (collapsed by default)
|
|
16
14
|
*/
|
|
17
|
-
export function SubAgentDetails({
|
|
15
|
+
export function SubAgentDetails({ parentStatus, agentName, query, isExpanded: controlledIsExpanded, onExpandChange, storedMessages, }) {
|
|
18
16
|
const [internalIsExpanded, setInternalIsExpanded] = useState(false);
|
|
19
17
|
// Use controlled state if provided, otherwise use internal state
|
|
20
18
|
const isExpanded = controlledIsExpanded ?? internalIsExpanded;
|
|
@@ -23,31 +21,22 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
|
|
|
23
21
|
const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
|
|
24
22
|
const [isNearBottom, setIsNearBottom] = useState(true);
|
|
25
23
|
const thinkingContainerRef = useRef(null);
|
|
26
|
-
//
|
|
27
|
-
const
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
: streamedMessages;
|
|
43
|
-
// Use parent status as primary indicator, fall back to hook's streaming state
|
|
44
|
-
// Parent is "in_progress" means sub-agent is definitely still running
|
|
45
|
-
// In replay mode, we're never running
|
|
46
|
-
const isRunning = isReplay
|
|
47
|
-
? false
|
|
48
|
-
: parentStatus === "in_progress" ||
|
|
49
|
-
parentStatus === "pending" ||
|
|
50
|
-
hookIsStreaming;
|
|
24
|
+
// Use messages from storedMessages prop (populated by parent via tool_call_update)
|
|
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
|
+
// Determine if subagent is still running based on parent status
|
|
39
|
+
const isRunning = parentStatus === "in_progress" || parentStatus === "pending";
|
|
51
40
|
// Get the current/latest message
|
|
52
41
|
const currentMessage = messages[messages.length - 1];
|
|
53
42
|
const hasContent = currentMessage &&
|
|
@@ -55,6 +44,13 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
|
|
|
55
44
|
(currentMessage.toolCalls && currentMessage.toolCalls.length > 0) ||
|
|
56
45
|
(currentMessage.contentBlocks &&
|
|
57
46
|
currentMessage.contentBlocks.length > 0));
|
|
47
|
+
console.log("[SUBAGENT] SubAgentDetails render state", {
|
|
48
|
+
agentName,
|
|
49
|
+
messageCount: messages.length,
|
|
50
|
+
hasContent,
|
|
51
|
+
isRunning,
|
|
52
|
+
parentStatus,
|
|
53
|
+
});
|
|
58
54
|
// Auto-collapse Thinking when completed (so Output is the primary view)
|
|
59
55
|
const prevIsRunningRef = useRef(isRunning);
|
|
60
56
|
useEffect(() => {
|
|
@@ -125,7 +121,7 @@ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName
|
|
|
125
121
|
] })), _jsxs("div", { children: [
|
|
126
122
|
_jsxs("button", { type: "button", onClick: () => setIsThinkingExpanded(!isThinkingExpanded), className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group", children: [
|
|
127
123
|
_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider font-sans", children: "Stream" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isThinkingExpanded ? "rotate-180" : ""}` })
|
|
128
|
-
] }), isThinkingExpanded && (_jsxs("div", { ref: thinkingContainerRef, className: "mt-2 rounded-md overflow-hidden bg-muted/30 border border-border/50 max-h-[200px] overflow-y-auto", children: [
|
|
124
|
+
] }), isThinkingExpanded && (_jsxs("div", { ref: thinkingContainerRef, className: "mt-2 rounded-md overflow-hidden bg-muted/30 border border-border/50 max-h-[200px] overflow-y-auto", children: [!hasContent && isRunning && (_jsx("div", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: "Waiting for sub-agent response..." })), currentMessage && (_jsxs("div", { className: "px-2 py-2 space-y-2", children: [currentMessage.contentBlocks &&
|
|
129
125
|
currentMessage.contentBlocks.length > 0
|
|
130
126
|
? // Render interleaved content blocks
|
|
131
127
|
currentMessage.contentBlocks.map((block, blockIdx) => block.type === "text" ? (_jsx("div", { className: "text-[11px] text-foreground prose prose-sm dark:prose-invert max-w-none prose-p:my-1 prose-pre:my-1 prose-code:text-[10px]", children: _jsx(MarkdownRenderer, { content: block.text }) }, `text-${block.text.slice(0, 30)}-${blockIdx}`)) : (_jsx(SubagentToolCallItem, { toolCall: block.toolCall }, block.toolCall.id)))
|
|
@@ -138,12 +138,9 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
138
138
|
(n.metadata?.action === "truncated" ||
|
|
139
139
|
n.metadata?.action === "compacted_then_truncated")))
|
|
140
140
|
: false;
|
|
141
|
-
// Detect subagent calls
|
|
142
|
-
const
|
|
143
|
-
const hasStoredSubagent = !!(singleToolCall?.subagentMessages &&
|
|
141
|
+
// Detect subagent calls (subagents now run in-process, messages in subagentMessages)
|
|
142
|
+
const isSubagentCall = !!(singleToolCall?.subagentMessages &&
|
|
144
143
|
singleToolCall.subagentMessages.length > 0);
|
|
145
|
-
const isSubagentCall = hasLiveSubagent || hasStoredSubagent;
|
|
146
|
-
const isReplaySubagent = hasStoredSubagent;
|
|
147
144
|
// State for subagent expansion
|
|
148
145
|
const [isSubagentExpanded, setIsSubagentExpanded] = useState(false);
|
|
149
146
|
// Safely access ChatLayout context
|
|
@@ -312,7 +309,7 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
312
309
|
return (_jsxs("span", { className: "text-paragraph-sm text-text-secondary/70 pl-4.5", children: [compactedCount, " response", compactedCount > 1 ? "s" : "", " ", "compacted"] }));
|
|
313
310
|
}
|
|
314
311
|
return null;
|
|
315
|
-
})()] }), !isTodoWrite && isSubagentCall && singleToolCall && (_jsx("div", { className: "pl-4.5 mt-2", children: _jsx(SubAgentDetails, {
|
|
312
|
+
})()] }), !isTodoWrite && isSubagentCall && singleToolCall && (_jsx("div", { className: "pl-4.5 mt-2", children: _jsx(SubAgentDetails, { parentStatus: singleToolCall.status, agentName: singleToolCall.rawInput?.agentName, query: singleToolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded, storedMessages: singleToolCall.subagentMessages }) })), !isTodoWrite && isExpanded && (_jsx("div", { className: "mt-1", children: isGrouped ? (
|
|
316
313
|
// Render individual tool calls in group
|
|
317
314
|
_jsx("div", { className: "flex flex-col gap-4 mt-2", children: toolCalls.map((toolCall) => {
|
|
318
315
|
const hookNotification = hookNotifications.find((n) => n.toolCallId === toolCall.id);
|
|
@@ -329,11 +326,8 @@ export function ToolOperation({ toolCalls, isGrouped = false, autoMinimize = tru
|
|
|
329
326
|
*/
|
|
330
327
|
function GroupedToolCallItem({ toolCall, hookNotification, }) {
|
|
331
328
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
332
|
-
// Detect subagent calls
|
|
333
|
-
const
|
|
334
|
-
const hasStoredSubagent = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
|
|
335
|
-
const isSubagentCall = hasLiveSubagent || hasStoredSubagent;
|
|
336
|
-
const isReplaySubagent = hasStoredSubagent;
|
|
329
|
+
// Detect subagent calls (subagents now run in-process, messages in subagentMessages)
|
|
330
|
+
const isSubagentCall = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
|
|
337
331
|
// Detect compaction for this individual tool call
|
|
338
332
|
const hasCompaction = !!((hookNotification?.status === "completed" &&
|
|
339
333
|
hookNotification.metadata?.action &&
|
|
@@ -365,7 +359,7 @@ function GroupedToolCallItem({ toolCall, hookNotification, }) {
|
|
|
365
359
|
: "Response was compacted";
|
|
366
360
|
})() })
|
|
367
361
|
] }) })), _jsx(ChevronDown, { className: `h-3 w-3 text-text-secondary/70 group-hover:text-text-secondary transition-colors transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` })
|
|
368
|
-
] }), _jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, {
|
|
362
|
+
] }), _jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, { parentStatus: toolCall.status, agentName: toolCall.rawInput?.agentName, query: toolCall.rawInput?.query, isExpanded: isExpanded, onExpandChange: setIsExpanded, storedMessages: toolCall.subagentMessages }) })
|
|
369
363
|
] }));
|
|
370
364
|
}
|
|
371
365
|
// Regular tool call - collapsible with clickable header
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.106",
|
|
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.84",
|
|
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.103",
|
|
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
|
-
}
|