@townco/ui 0.1.122 → 0.1.124

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.
@@ -287,7 +287,10 @@ const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChan
287
287
  if (!hasInitialScrolledRef.current) {
288
288
  // Use a small delay to let initial content render
289
289
  const timeout = setTimeout(() => {
290
- container.scrollTop = container.scrollHeight;
290
+ // Only scroll if the content actually overflows the viewport.
291
+ if (container.scrollHeight > container.clientHeight + 1) {
292
+ container.scrollTop = container.scrollHeight;
293
+ }
291
294
  hasInitialScrolledRef.current = true;
292
295
  }, 100);
293
296
  return () => clearTimeout(timeout);
@@ -375,6 +375,11 @@ export function useScrollToBottom() {
375
375
  return;
376
376
  }
377
377
  const scrollIfNeeded = () => {
378
+ // If content doesn't overflow, don't force any programmatic scroll.
379
+ // This avoids "snapping" the first/small streaming message to the top.
380
+ if (container.scrollHeight <= container.clientHeight + 1) {
381
+ return;
382
+ }
378
383
  // Only auto-scroll if user was at bottom and isn't actively scrolling
379
384
  if (isAtBottomRef.current && !isUserScrollingRef.current) {
380
385
  requestAnimationFrame(() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.122",
3
+ "version": "0.1.124",
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.100",
52
+ "@townco/core": "0.0.102",
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.119",
70
+ "@townco/tsconfig": "0.1.121",
71
71
  "@types/node": "^24.10.0",
72
72
  "@types/react": "^19.2.2",
73
73
  "@types/unist": "^3.0.3",
@@ -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,23 +0,0 @@
1
- export interface SubagentStreamProps {
2
- /** Sub-agent HTTP port */
3
- port: number;
4
- /** Sub-agent session ID */
5
- sessionId: string;
6
- /** Optional host (defaults to localhost) */
7
- host?: string;
8
- /** Parent tool call status - use this to determine if sub-agent is running */
9
- parentStatus?: "pending" | "in_progress" | "completed" | "failed";
10
- /** Sub-agent name (for display) */
11
- agentName?: string | undefined;
12
- /** Query sent to the sub-agent */
13
- query?: string | undefined;
14
- }
15
- /**
16
- * SubagentStream component - displays streaming content from a sub-agent.
17
- *
18
- * This component:
19
- * - Connects directly to the sub-agent's SSE endpoint
20
- * - Displays streaming text and tool calls
21
- * - Renders in a collapsible section (collapsed by default)
22
- */
23
- export declare function SubagentStream({ port, sessionId, host, parentStatus, agentName, query, }: SubagentStreamProps): import("react/jsx-runtime").JSX.Element;
@@ -1,98 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { ChevronDown, CircleDot, Loader2 } from "lucide-react";
3
- import React, { useCallback, useEffect, useRef, useState } from "react";
4
- import { useSubagentStream } from "../../core/hooks/use-subagent-stream.js";
5
- const SCROLL_THRESHOLD = 50; // px from bottom to consider "at bottom"
6
- /**
7
- * SubagentStream component - displays streaming content from a sub-agent.
8
- *
9
- * This component:
10
- * - Connects directly to the sub-agent's SSE endpoint
11
- * - Displays streaming text and tool calls
12
- * - Renders in a collapsible section (collapsed by default)
13
- */
14
- export function SubagentStream({ port, sessionId, host, parentStatus, agentName, query, }) {
15
- const [isExpanded, setIsExpanded] = useState(false); // Start collapsed for parallel ops
16
- const [isThinkingExpanded, setIsThinkingExpanded] = useState(true);
17
- const [isNearBottom, setIsNearBottom] = useState(true);
18
- const thinkingContainerRef = useRef(null);
19
- const { messages, isStreaming: hookIsStreaming, error } = useSubagentStream({
20
- port,
21
- sessionId,
22
- ...(host !== undefined ? { host } : {}),
23
- });
24
- // Use parent status as primary indicator, fall back to hook's streaming state
25
- // Parent is "in_progress" means sub-agent is definitely still running
26
- const isRunning = parentStatus === "in_progress" || parentStatus === "pending" || hookIsStreaming;
27
- // Get the current/latest message
28
- const currentMessage = messages[messages.length - 1];
29
- const hasContent = currentMessage &&
30
- (currentMessage.content ||
31
- (currentMessage.toolCalls && currentMessage.toolCalls.length > 0));
32
- // Auto-collapse Thinking when completed (so Output is the primary view)
33
- const prevIsRunningRef = useRef(isRunning);
34
- useEffect(() => {
35
- if (prevIsRunningRef.current && !isRunning) {
36
- // Just completed - collapse thinking to show output
37
- setIsThinkingExpanded(false);
38
- }
39
- prevIsRunningRef.current = isRunning;
40
- }, [isRunning]);
41
- // Check if user is near bottom of scroll area
42
- const checkScrollPosition = useCallback(() => {
43
- const container = thinkingContainerRef.current;
44
- if (!container)
45
- return;
46
- const { scrollTop, scrollHeight, clientHeight } = container;
47
- const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
48
- setIsNearBottom(distanceFromBottom < SCROLL_THRESHOLD);
49
- }, []);
50
- // Scroll to bottom
51
- const scrollToBottom = useCallback(() => {
52
- const container = thinkingContainerRef.current;
53
- if (!container)
54
- return;
55
- container.scrollTop = container.scrollHeight;
56
- }, []);
57
- // Auto-scroll when content changes and user is near bottom
58
- useEffect(() => {
59
- if (isNearBottom && (isRunning || hasContent)) {
60
- scrollToBottom();
61
- }
62
- }, [currentMessage?.content, currentMessage?.toolCalls, isNearBottom, isRunning, hasContent, scrollToBottom]);
63
- // Set up scroll listener
64
- useEffect(() => {
65
- const container = thinkingContainerRef.current;
66
- if (!container)
67
- return;
68
- const handleScroll = () => checkScrollPosition();
69
- container.addEventListener("scroll", handleScroll, { passive: true });
70
- checkScrollPosition(); // Check initial position
71
- return () => container.removeEventListener("scroll", handleScroll);
72
- }, [checkScrollPosition, isThinkingExpanded, isExpanded]);
73
- // Get last line of streaming content for preview
74
- const lastLine = currentMessage?.content
75
- ? currentMessage.content.split("\n").filter(Boolean).pop() || ""
76
- : "";
77
- const previewText = lastLine.length > 100 ? `${lastLine.slice(0, 100)}...` : lastLine;
78
- return (_jsxs("div", { children: [!isExpanded && (_jsx("button", { type: "button", onClick: () => setIsExpanded(true), className: "w-full max-w-md text-left cursor-pointer bg-transparent border-none p-0", children: previewText ? (_jsx("p", { className: `text-paragraph-sm text-muted-foreground truncate ${isRunning ? "animate-pulse" : ""}`, children: previewText })) : isRunning ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 italic animate-pulse", children: "Waiting for response..." })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsxs("div", { className: "text-[11px] font-mono space-y-1", children: [agentName && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "agentName: " }), _jsx("span", { className: "text-foreground", children: agentName })] })), query && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "query: " }), _jsx("span", { className: "text-foreground", children: query })] }))] })] })), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setIsThinkingExpanded(!isThinkingExpanded), className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider font-sans", children: "Thinking" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isThinkingExpanded ? "rotate-180" : ""}` })] }), isThinkingExpanded && (_jsxs("div", { ref: thinkingContainerRef, className: "mt-2 rounded-md overflow-hidden bg-muted/30 border border-border/50 max-h-[200px] overflow-y-auto", children: [error && (_jsxs("div", { className: "px-2 py-2 text-[11px] text-destructive", children: ["Error: ", error] })), !error && !hasContent && isRunning && (_jsx("div", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: "Waiting for sub-agent response..." })), currentMessage && (_jsxs("div", { className: "px-2 py-2 space-y-2", children: [currentMessage.toolCalls &&
79
- currentMessage.toolCalls.length > 0 && (_jsx("div", { className: "space-y-1", children: currentMessage.toolCalls.map((tc) => (_jsx(SubagentToolCallItem, { toolCall: tc }, tc.id))) })), currentMessage.content && (_jsxs("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono", children: [currentMessage.content, currentMessage.isStreaming && (_jsx("span", { className: "inline-block w-1.5 h-3 bg-primary/70 ml-0.5 animate-pulse" }))] }))] }))] }))] }), !isRunning && currentMessage?.content && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono max-h-[200px] overflow-y-auto rounded-md bg-muted/30 border border-border/50 px-2 py-2", children: currentMessage.content })] }))] }))] }));
80
- }
81
- /**
82
- * Simple tool call display for sub-agent tool calls
83
- */
84
- function SubagentToolCallItem({ toolCall }) {
85
- const statusIcon = {
86
- pending: "...",
87
- in_progress: "",
88
- completed: "",
89
- failed: "",
90
- }[toolCall.status];
91
- const statusColor = {
92
- pending: "text-muted-foreground",
93
- in_progress: "text-blue-500",
94
- completed: "text-green-500",
95
- failed: "text-destructive",
96
- }[toolCall.status];
97
- return (_jsxs("div", { className: "flex items-center gap-2 text-[10px] text-muted-foreground", children: [_jsx("span", { className: statusColor, children: statusIcon }), _jsx("span", { className: "font-medium", children: toolCall.prettyName || toolCall.title }), toolCall.status === "in_progress" && (_jsx(Loader2, { className: "h-2.5 w-2.5 animate-spin" }))] }));
98
- }
@@ -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,234 +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
- // A subagent call can be detected by:
48
- // - Live: has port and sessionId (but no stored messages yet)
49
- // - Replay: has stored subagentMessages
50
- const hasLiveSubagent = !!(toolCall.subagentPort && toolCall.subagentSessionId);
51
- const hasStoredSubagent = !!(toolCall.subagentMessages && toolCall.subagentMessages.length > 0);
52
- const isSubagentCall = hasLiveSubagent || hasStoredSubagent;
53
- // Use replay mode if we have stored messages - they should take precedence
54
- // over trying to connect to SSE (which won't work for replayed sessions)
55
- const isReplaySubagent = hasStoredSubagent;
56
- // Safely access ChatLayout context - will be undefined if not within ChatLayout
57
- const layoutContext = React.useContext(ChatLayout.Context);
58
- // Click handler: toggle sidepanel for TodoWrite, subagent details for subagents, expand for others
59
- const handleHeaderClick = React.useCallback(() => {
60
- if (isTodoWrite && layoutContext) {
61
- // Toggle sidepanel - close if already open on todo tab, otherwise open
62
- if (layoutContext.panelSize !== "hidden" &&
63
- layoutContext.activeTab === "todo") {
64
- layoutContext.setPanelSize("hidden");
65
- }
66
- else {
67
- layoutContext.setPanelSize("small");
68
- layoutContext.setActiveTab("todo");
69
- }
70
- }
71
- else if (isSubagentCall) {
72
- // Toggle subagent details
73
- setIsSubagentExpanded(!isSubagentExpanded);
74
- }
75
- else {
76
- // Normal expand/collapse
77
- setIsExpanded(!isExpanded);
78
- }
79
- }, [
80
- isTodoWrite,
81
- layoutContext,
82
- isExpanded,
83
- isSubagentCall,
84
- isSubagentExpanded,
85
- ]);
86
- // Determine which icon to show
87
- const IconComponent = toolCall.icon && ICON_MAP[toolCall.icon]
88
- ? ICON_MAP[toolCall.icon]
89
- : CircleDot;
90
- // Determine display name
91
- const displayName = toolCall.prettyName || toolCall.title;
92
- // Determine icon color based on status (especially for subagents)
93
- const isSubagentRunning = isSubagentCall &&
94
- (toolCall.status === "in_progress" || toolCall.status === "pending");
95
- const isSubagentFailed = isSubagentCall && toolCall.status === "failed";
96
- const iconColorClass = isSubagentCall
97
- ? isSubagentFailed
98
- ? "text-destructive"
99
- : isSubagentRunning
100
- ? "text-foreground animate-pulse"
101
- : "text-green-500"
102
- : "text-muted-foreground";
103
- const statusTooltip = isSubagentCall
104
- ? isSubagentFailed
105
- ? "Sub-agent failed"
106
- : isSubagentRunning
107
- ? "Sub-agent running"
108
- : "Sub-agent completed"
109
- : undefined;
110
- // Check if there's an error
111
- const hasError = toolCall.status === "failed" || !!toolCall.error;
112
- // Check if this is a preliminary (pending) tool call without full details yet
113
- const isPreliminary = toolCall.status === "pending" &&
114
- (!toolCall.rawInput || Object.keys(toolCall.rawInput).length === 0);
115
- // JSON View style based on theme
116
- const jsonStyle = {
117
- fontSize: "11px",
118
- backgroundColor: "transparent",
119
- fontFamily: "inherit",
120
- "--w-rjv-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
121
- "--w-rjv-key-string": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
122
- "--w-rjv-background-color": "transparent",
123
- "--w-rjv-line-color": resolvedTheme === "dark" ? "#27272a" : "#e4e4e7",
124
- "--w-rjv-arrow-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
125
- "--w-rjv-edit-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
126
- "--w-rjv-info-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
127
- "--w-rjv-update-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
128
- "--w-rjv-copied-color": resolvedTheme === "dark" ? "#fafafa" : "#09090b",
129
- "--w-rjv-copied-success-color": resolvedTheme === "dark" ? "#22c55e" : "#16a34a",
130
- "--w-rjv-curlybraces-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
131
- "--w-rjv-colon-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
132
- "--w-rjv-brackets-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
133
- "--w-rjv-quotes-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
134
- "--w-rjv-quotes-string-color": resolvedTheme === "dark" ? "#a1a1aa" : "#71717a",
135
- "--w-rjv-type-string-color": resolvedTheme === "dark" ? "#22c55e" : "#16a34a",
136
- "--w-rjv-type-int-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
137
- "--w-rjv-type-float-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
138
- "--w-rjv-type-bigint-color": resolvedTheme === "dark" ? "#f59e0b" : "#d97706",
139
- "--w-rjv-type-boolean-color": resolvedTheme === "dark" ? "#3b82f6" : "#2563eb",
140
- "--w-rjv-type-date-color": resolvedTheme === "dark" ? "#ec4899" : "#db2777",
141
- "--w-rjv-type-url-color": resolvedTheme === "dark" ? "#3b82f6" : "#2563eb",
142
- "--w-rjv-type-null-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
143
- "--w-rjv-type-nan-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
144
- "--w-rjv-type-undefined-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
145
- };
146
- // Preliminary tool calls show as simple light gray text without expansion
147
- if (isPreliminary) {
148
- return (_jsx("div", { className: "flex flex-col my-4", children: _jsxs("span", { className: "text-paragraph-sm text-muted-foreground/50", children: ["Invoking ", displayName] }) }));
149
- }
150
- 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, storedMessages: toolCall.subagentMessages, isReplay: isReplaySubagent }) })), !isTodoWrite && !isSubagentCall && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.rawInput &&
151
- Object.keys(toolCall.rawInput).length > 0 &&
152
- !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 &&
153
- loc.line !== undefined &&
154
- `:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), (toolCall.content && toolCall.content.length > 0) ||
155
- 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) => {
156
- // Generate a stable key based on content
157
- const getBlockKey = () => {
158
- if (block.type === "diff" && "path" in block) {
159
- return `diff-${block.path}-${idx}`;
160
- }
161
- if (block.type === "terminal" && "terminalId" in block) {
162
- return `terminal-${block.terminalId}`;
163
- }
164
- if (block.type === "text" && "text" in block) {
165
- return `text-${block.text.substring(0, 20)}-${idx}`;
166
- }
167
- if (block.type === "content" && "content" in block) {
168
- const innerContent = block.content;
169
- return `content-${innerContent.text?.substring(0, 20)}-${idx}`;
170
- }
171
- return `block-${idx}`;
172
- };
173
- // Helper to render text content (with JSON parsing if applicable)
174
- const renderTextContent = (text, key) => {
175
- // Try to parse as JSON
176
- try {
177
- const parsed = JSON.parse(text);
178
- // If it's an object or array, render with JsonView
179
- if (typeof parsed === "object" && parsed !== null) {
180
- return (_jsx("div", { className: "text-[11px]", children: _jsx(JsonView, { value: parsed, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) }, key));
181
- }
182
- }
183
- catch {
184
- // Not valid JSON, render as plain text
185
- }
186
- // Render as plain text
187
- return (_jsx("pre", { className: "whitespace-pre-wrap font-mono text-[11px] text-foreground overflow-x-auto", children: text }, key));
188
- };
189
- // Handle nested content blocks (ACP format)
190
- if (block.type === "content" && "content" in block) {
191
- const innerContent = block.content;
192
- if (innerContent.type === "text" && innerContent.text) {
193
- return renderTextContent(innerContent.text, getBlockKey());
194
- }
195
- }
196
- // Handle direct text blocks
197
- if (block.type === "text" && "text" in block) {
198
- return renderTextContent(block.text, getBlockKey());
199
- }
200
- // Handle image blocks
201
- if (block.type === "image") {
202
- const alt = block.alt || "Generated image";
203
- let imageSrc;
204
- if ("data" in block) {
205
- // Base64 encoded image
206
- const mimeType = block.mimeType || "image/png";
207
- imageSrc = `data:${mimeType};base64,${block.data}`;
208
- }
209
- else if ("url" in block) {
210
- // URL or file path
211
- imageSrc = block.url;
212
- }
213
- else {
214
- return null;
215
- }
216
- 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()));
217
- }
218
- // Handle diff blocks
219
- if (block.type === "diff" &&
220
- "path" in block &&
221
- "oldText" in block &&
222
- "newText" in block) {
223
- 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 &&
224
- block.line !== null &&
225
- block.line !== undefined &&
226
- `:${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()));
227
- }
228
- // Handle terminal blocks
229
- if (block.type === "terminal" && "terminalId" in block) {
230
- return (_jsxs("div", { className: "bg-neutral-900 text-neutral-100 p-2 rounded text-[11px] font-mono", children: ["Terminal: ", block.terminalId] }, getBlockKey()));
231
- }
232
- return null;
233
- }), 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)"] }))] }))] }))] }))] }));
234
- }
@@ -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
- }