@townco/ui 0.1.48 → 0.1.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/hooks/index.d.ts +1 -0
- package/dist/core/hooks/index.js +1 -0
- package/dist/core/hooks/use-chat-input.d.ts +2 -0
- package/dist/core/hooks/use-chat-input.js +11 -1
- package/dist/core/hooks/use-chat-messages.d.ts +22 -1
- package/dist/core/hooks/use-chat-messages.js +19 -4
- package/dist/core/hooks/use-chat-session.d.ts +1 -1
- package/dist/core/hooks/use-chat-session.js +22 -0
- package/dist/core/hooks/use-message-history.d.ts +12 -0
- package/dist/core/hooks/use-message-history.js +113 -0
- package/dist/core/hooks/use-tool-calls.d.ts +11 -0
- package/dist/core/schemas/chat.d.ts +40 -0
- package/dist/core/schemas/chat.js +9 -0
- package/dist/core/schemas/tool-call.d.ts +34 -0
- package/dist/core/schemas/tool-call.js +27 -0
- package/dist/core/store/chat-store.d.ts +5 -0
- package/dist/core/store/chat-store.js +46 -0
- package/dist/gui/components/ChatEmptyState.d.ts +4 -0
- package/dist/gui/components/ChatEmptyState.js +2 -2
- package/dist/gui/components/ChatInput.d.ts +21 -1
- package/dist/gui/components/ChatInput.js +184 -7
- package/dist/gui/components/ChatLayout.d.ts +3 -2
- package/dist/gui/components/ChatLayout.js +7 -7
- package/dist/gui/components/ChatPanelTabContent.d.ts +17 -0
- package/dist/gui/components/ChatPanelTabContent.js +6 -5
- package/dist/gui/components/ChatView.d.ts +3 -1
- package/dist/gui/components/ChatView.js +81 -49
- package/dist/gui/components/InvokingGroup.d.ts +9 -0
- package/dist/gui/components/InvokingGroup.js +16 -0
- package/dist/gui/components/MessageContent.js +122 -6
- package/dist/gui/components/PanelTabsHeader.d.ts +1 -1
- package/dist/gui/components/PanelTabsHeader.js +6 -1
- package/dist/gui/components/Response.js +2 -0
- package/dist/gui/components/Task.js +3 -3
- package/dist/gui/components/TodoListItem.js +1 -1
- package/dist/gui/components/ToolCall.js +57 -5
- package/dist/gui/components/ToolCallGroup.d.ts +8 -0
- package/dist/gui/components/ToolCallGroup.js +29 -0
- package/dist/gui/components/index.d.ts +1 -2
- package/dist/gui/components/index.js +1 -2
- package/dist/sdk/client/acp-client.d.ts +24 -1
- package/dist/sdk/client/acp-client.js +28 -7
- package/dist/sdk/schemas/message.d.ts +41 -9
- package/dist/sdk/schemas/message.js +15 -3
- package/dist/sdk/schemas/session.d.ts +81 -16
- package/dist/sdk/transports/http.d.ts +14 -0
- package/dist/sdk/transports/http.js +130 -36
- package/dist/sdk/transports/stdio.d.ts +14 -0
- package/dist/sdk/transports/stdio.js +27 -10
- package/dist/sdk/transports/types.d.ts +29 -0
- package/package.json +7 -3
|
@@ -1,26 +1,68 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createLogger } from "@townco/core";
|
|
3
|
-
import { ArrowUp, ChevronUp, Code, PanelRight, Settings, Sparkles, } from "lucide-react";
|
|
3
|
+
import { ArrowUp, Bug, ChevronUp, Code, PanelRight, Settings, Sparkles, X, } from "lucide-react";
|
|
4
4
|
import { useEffect, useState } from "react";
|
|
5
5
|
import { useChatMessages, useChatSession, useToolCalls, } from "../../core/hooks/index.js";
|
|
6
|
-
import { useChatStore } from "../../core/store/chat-store.js";
|
|
6
|
+
import { selectTodosForCurrentSession, useChatStore, } from "../../core/store/chat-store.js";
|
|
7
7
|
import { calculateTokenPercentage, formatTokenPercentage, } from "../../core/utils/model-context.js";
|
|
8
8
|
import { cn } from "../lib/utils.js";
|
|
9
|
-
import { ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, FilesTabContent, IconButton, Message, MessageContent, PanelTabsHeader, SourcesTabContent, Tabs, TabsContent, ThemeToggle, TodoTabContent, } from "./index.js";
|
|
9
|
+
import { ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, FilesTabContent, IconButton, Message, MessageContent, PanelTabsHeader, SettingsTabContent, SourcesTabContent, Tabs, TabsContent, ThemeToggle, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./index.js";
|
|
10
10
|
const logger = createLogger("gui");
|
|
11
11
|
// Helper component to provide openFiles callback
|
|
12
12
|
function OpenFilesButton({ children, }) {
|
|
13
|
-
const { setPanelSize, setActiveTab } = ChatLayout.useChatLayoutContext();
|
|
13
|
+
const { setPanelSize, setActiveTab, panelSize, activeTab } = ChatLayout.useChatLayoutContext();
|
|
14
14
|
const openFiles = () => {
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
if (panelSize !== "hidden" && activeTab === "files") {
|
|
16
|
+
setPanelSize("hidden");
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
setPanelSize("small");
|
|
20
|
+
setActiveTab("files");
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const openSettings = () => {
|
|
24
|
+
if (panelSize !== "hidden" && activeTab === "settings") {
|
|
25
|
+
setPanelSize("hidden");
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
setPanelSize("small");
|
|
29
|
+
setActiveTab("settings");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
return _jsx(_Fragment, { children: children({ openFiles, openSettings }) });
|
|
33
|
+
}
|
|
34
|
+
// Hook to handle sidebar keyboard shortcut (Cmd+B / Ctrl+B)
|
|
35
|
+
function SidebarHotkey() {
|
|
36
|
+
const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const handleKeyDown = (e) => {
|
|
39
|
+
// Cmd+B (Mac) or Ctrl+B (Windows/Linux)
|
|
40
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "b") {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
setPanelSize(panelSize === "hidden" ? "small" : "hidden");
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
46
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
47
|
+
}, [panelSize, setPanelSize]);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
// Chat input with attachment handling
|
|
51
|
+
function ChatInputWithAttachments({ client, startSession, placeholder, latestContextSize, currentModel, commandMenuItems, }) {
|
|
52
|
+
const attachedFiles = useChatStore((state) => state.input.attachedFiles);
|
|
53
|
+
const addFileAttachment = useChatStore((state) => state.addFileAttachment);
|
|
54
|
+
const removeFileAttachment = useChatStore((state) => state.removeFileAttachment);
|
|
55
|
+
const handleFilesSelected = (files) => {
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
addFileAttachment(file);
|
|
58
|
+
}
|
|
17
59
|
};
|
|
18
|
-
return _jsx(
|
|
60
|
+
return (_jsxs(ChatInputRoot, { client: client, startSession: startSession, children: [_jsx(ChatInputCommandMenu, { commands: commandMenuItems }), attachedFiles.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2 p-3 border-b border-border", children: attachedFiles.map((file, index) => (_jsxs("div", { className: "relative group rounded-md overflow-hidden border border-border", children: [_jsx("img", { src: `data:${file.mimeType};base64,${file.data}`, alt: file.name, className: "h-20 w-20 object-cover" }), _jsx("button", { type: "button", onClick: () => removeFileAttachment(index), className: "absolute top-1 right-1 p-1 rounded-full bg-background/80 hover:bg-background opacity-0 group-hover:opacity-100 transition-opacity", children: _jsx(X, { className: "size-3" }) })] }, index))) })), _jsx(ChatInputField, { placeholder: placeholder, autoFocus: true, onFilesDropped: handleFilesSelected }), _jsxs(ChatInputToolbar, { children: [_jsxs("div", { className: "flex items-baseline gap-1", children: [_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, { onFilesSelected: handleFilesSelected }), latestContextSize != null && (_jsx(ContextUsageButton, { contextSize: latestContextSize, modelContextWindow: currentModel?.includes("claude") ? 200000 : 128000 }))] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputVoiceInput, {}), _jsx(ChatInputSubmit, { children: _jsx(ArrowUp, { className: "size-4" }) })] })] })] }));
|
|
19
61
|
}
|
|
20
62
|
// Controlled Tabs component for the aside panel
|
|
21
|
-
function AsideTabs() {
|
|
63
|
+
function AsideTabs({ todos, tools, mcps, subagents, }) {
|
|
22
64
|
const { activeTab, setActiveTab } = ChatLayout.useChatLayoutContext();
|
|
23
|
-
return (_jsxs(Tabs, { value: activeTab, onValueChange: (value) => setActiveTab(value), className: "flex flex-col h-full", children: [_jsx("div", { className: cn("border-b border-border bg-card", "px-6 py-2 h-16", "flex items-center", "[border-bottom-width:0.5px]"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0", children: _jsx(TodoTabContent, {}) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0", children: _jsx(SourcesTabContent, {}) })] }));
|
|
65
|
+
return (_jsxs(Tabs, { value: activeTab, onValueChange: (value) => setActiveTab(value), className: "flex flex-col h-full", children: [_jsx("div", { className: cn("border-b border-border bg-card", "px-6 py-2 h-16", "flex items-center", "[border-bottom-width:0.5px]"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0", children: _jsx(SourcesTabContent, {}) }), _jsx(TabsContent, { value: "settings", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })] }));
|
|
24
66
|
}
|
|
25
67
|
// Mobile header component that uses ChatHeader context
|
|
26
68
|
function MobileHeader({ agentName, showHeader, }) {
|
|
@@ -28,13 +70,14 @@ function MobileHeader({ agentName, showHeader, }) {
|
|
|
28
70
|
return (_jsxs("div", { className: "flex lg:hidden items-center flex-1", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx("h1", { className: "text-heading-4 text-foreground", children: agentName }) })), !showHeader && _jsx("div", { className: "flex-1" }), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle menu", onClick: () => setIsExpanded(!isExpanded), children: _jsx(ChevronUp, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isExpanded ? "" : "rotate-180") }) })] }));
|
|
29
71
|
}
|
|
30
72
|
// Header component that uses ChatLayout context (must be inside ChatLayout.Root)
|
|
31
|
-
function AppChatHeader({ agentName, todos, sources, showHeader, }) {
|
|
73
|
+
function AppChatHeader({ agentName, todos, sources, showHeader, sessionId, debuggerUrl, tools, mcps, subagents, }) {
|
|
32
74
|
const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
|
|
33
|
-
|
|
75
|
+
const debuggerLink = sessionId && debuggerUrl ? `${debuggerUrl}/sessions/${sessionId}` : null;
|
|
76
|
+
return (_jsxs(ChatHeader.Root, { className: cn("border-b border-border bg-card relative lg:p-0", "[border-bottom-width:0.5px]"), children: [_jsxs("div", { className: "hidden lg:flex items-center w-full h-16 py-5 pl-6 pr-4", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx("h1", { className: "text-heading-4 text-foreground", children: agentName }) })), !showHeader && _jsx("div", { className: "flex-1" }), debuggerUrl && (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(IconButton, { "aria-label": "View session in debugger", disabled: !debuggerLink, asChild: !!debuggerLink, children: debuggerLink ? (_jsx("a", { href: debuggerLink, target: "_blank", rel: "noopener noreferrer", children: _jsx(Bug, { className: "size-4 text-muted-foreground" }) })) : (_jsx(Bug, { className: "size-4 text-muted-foreground" })) }) }), _jsx(TooltipContent, { children: _jsx("p", { children: "View session in debugger" }) })] }) })), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle sidebar", onClick: () => {
|
|
34
77
|
setPanelSize(panelSize === "hidden" ? "small" : "hidden");
|
|
35
|
-
}, children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName, showHeader: showHeader }), _jsx(ChatHeader.ExpandablePanel, { className: cn("pt-6 pb-8 px-6", "border-b border-border bg-card", "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]", "[border-bottom-width:0.5px]"), children: _jsxs(Tabs, { defaultValue: "todo", className: "w-full", children: [_jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources"], variant: "default" }), _jsx(TabsContent, { value: "todo", className: "mt-4", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "mt-4", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "mt-4", children: _jsx(SourcesTabContent, { sources: sources }) })] }) })] }));
|
|
78
|
+
}, children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName, showHeader: showHeader }), _jsx(ChatHeader.ExpandablePanel, { className: cn("pt-6 pb-8 px-6", "border-b border-border bg-card", "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]", "[border-bottom-width:0.5px]"), children: _jsxs(Tabs, { defaultValue: "todo", className: "w-full", children: [_jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "default" }), _jsx(TabsContent, { value: "todo", className: "mt-4", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "mt-4", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "mt-4", children: _jsx(SourcesTabContent, { sources: sources }) }), _jsx(TabsContent, { value: "settings", className: "mt-4", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })] }) })] }));
|
|
36
79
|
}
|
|
37
|
-
export function ChatView({ client, initialSessionId, error: initError, }) {
|
|
80
|
+
export function ChatView({ client, initialSessionId, error: initError, debuggerUrl, }) {
|
|
38
81
|
// Use shared hooks from @townco/ui/core - MUST be called before any early returns
|
|
39
82
|
const { connectionStatus, connect, sessionId, startSession } = useChatSession(client, initialSessionId);
|
|
40
83
|
const { messages, sendMessage } = useChatMessages(client, startSession);
|
|
@@ -42,11 +85,14 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
|
|
|
42
85
|
const error = useChatStore((state) => state.error);
|
|
43
86
|
const currentModel = useChatStore((state) => state.currentModel);
|
|
44
87
|
const [agentName, setAgentName] = useState("Agent");
|
|
45
|
-
const [agentDescription, setAgentDescription] = useState("This agent can help you
|
|
88
|
+
const [agentDescription, setAgentDescription] = useState("This research agent can help you find and summarize information, analyze sources, track tasks, and answer questions about your research. Start by typing a message below to begin your investigation.");
|
|
46
89
|
const [suggestedPrompts, setSuggestedPrompts] = useState([
|
|
47
90
|
"Search the web for the latest news on top tech company earnings, produce a summary for each company, and then a macro trend analysis of the tech industry. Use your todo list",
|
|
48
91
|
"What can you help me with?",
|
|
49
92
|
]);
|
|
93
|
+
const [agentTools, setAgentTools] = useState([]);
|
|
94
|
+
const [agentMcps, setAgentMcps] = useState([]);
|
|
95
|
+
const [agentSubagents, setAgentSubagents] = useState([]);
|
|
50
96
|
const [isLargeScreen, setIsLargeScreen] = useState(typeof window !== "undefined" ? window.innerWidth >= 1024 : true);
|
|
51
97
|
const [placeholder, setPlaceholder] = useState("Type a message or / for commands...");
|
|
52
98
|
// Log connection status changes
|
|
@@ -80,6 +126,21 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
|
|
|
80
126
|
if (agentInfo.suggestedPrompts && agentInfo.suggestedPrompts.length > 0) {
|
|
81
127
|
setSuggestedPrompts(agentInfo.suggestedPrompts);
|
|
82
128
|
}
|
|
129
|
+
// Get tools, MCPs, and subagents
|
|
130
|
+
if (agentInfo.tools && agentInfo.tools.length > 0) {
|
|
131
|
+
setAgentTools(agentInfo.tools);
|
|
132
|
+
logger.info("Agent tools loaded", { tools: agentInfo.tools });
|
|
133
|
+
}
|
|
134
|
+
if (agentInfo.mcps && agentInfo.mcps.length > 0) {
|
|
135
|
+
setAgentMcps(agentInfo.mcps);
|
|
136
|
+
logger.info("Agent MCPs loaded", { mcps: agentInfo.mcps });
|
|
137
|
+
}
|
|
138
|
+
if (agentInfo.subagents && agentInfo.subagents.length > 0) {
|
|
139
|
+
setAgentSubagents(agentInfo.subagents);
|
|
140
|
+
logger.info("Agent subagents loaded", {
|
|
141
|
+
subagents: agentInfo.subagents,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
83
144
|
}
|
|
84
145
|
}, [client, sessionId, connectionStatus]);
|
|
85
146
|
// Monitor screen size changes and update isLargeScreen state
|
|
@@ -108,8 +169,7 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
|
|
|
108
169
|
if (initError) {
|
|
109
170
|
return (_jsx("div", { className: "flex items-center justify-center h-screen bg-background", children: _jsxs("div", { className: "text-center p-8 max-w-md", children: [_jsx("h1", { className: "text-2xl font-bold text-destructive mb-4", children: "Initialization Error" }), _jsx("p", { className: "text-foreground mb-4", children: initError }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Failed to initialize the ACP client. Check the console for details." })] }) }));
|
|
110
171
|
}
|
|
111
|
-
|
|
112
|
-
const todos = [];
|
|
172
|
+
const todos = useChatStore(selectTodosForCurrentSession);
|
|
113
173
|
// Dummy sources data based on Figma design
|
|
114
174
|
const sources = [
|
|
115
175
|
{
|
|
@@ -149,26 +209,6 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
|
|
|
149
209
|
const latestContextSize = useChatStore((state) => state.latestContextSize);
|
|
150
210
|
// Command menu items for chat input
|
|
151
211
|
const commandMenuItems = [
|
|
152
|
-
{
|
|
153
|
-
id: "model-sonnet",
|
|
154
|
-
label: "Use Sonnet 4.5",
|
|
155
|
-
description: "Switch to Claude Sonnet 4.5 model",
|
|
156
|
-
icon: _jsx(Sparkles, { className: "h-4 w-4" }),
|
|
157
|
-
category: "model",
|
|
158
|
-
onSelect: () => {
|
|
159
|
-
logger.info("User selected Sonnet 4.5 model");
|
|
160
|
-
},
|
|
161
|
-
},
|
|
162
|
-
{
|
|
163
|
-
id: "model-opus",
|
|
164
|
-
label: "Use Opus",
|
|
165
|
-
description: "Switch to Claude Opus model",
|
|
166
|
-
icon: _jsx(Sparkles, { className: "h-4 w-4" }),
|
|
167
|
-
category: "model",
|
|
168
|
-
onSelect: () => {
|
|
169
|
-
logger.info("User selected Opus model");
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
212
|
{
|
|
173
213
|
id: "settings",
|
|
174
214
|
label: "Open Settings",
|
|
@@ -179,22 +219,14 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
|
|
|
179
219
|
logger.info("User opened settings");
|
|
180
220
|
},
|
|
181
221
|
},
|
|
182
|
-
{
|
|
183
|
-
id: "code-mode",
|
|
184
|
-
label: "Code Mode",
|
|
185
|
-
description: "Enable code-focused responses",
|
|
186
|
-
icon: _jsx(Code, { className: "h-4 w-4" }),
|
|
187
|
-
category: "mode",
|
|
188
|
-
onSelect: () => {
|
|
189
|
-
logger.info("User enabled code mode");
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
222
|
];
|
|
193
|
-
return (_jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [_jsxs(ChatLayout.Main, { children: [_jsx(AppChatHeader, { agentName: agentName, todos: todos, sources: sources, showHeader: messages.length > 0 }), connectionStatus === "error" && error && (_jsx("div", { className: "border-b border-destructive/20 bg-destructive/10 px-6 py-4", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "mb-1 text-paragraph-sm font-semibold text-destructive", children: "Connection Error" }), _jsx("p", { className: "whitespace-pre-line text-paragraph-sm text-foreground", children: error })] }), _jsx("button", { type: "button", onClick: connect, className: "rounded-lg bg-destructive px-4 py-2 text-paragraph-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive-hover", children: "Retry" })] }) })), _jsxs(ChatLayout.Body, { children: [_jsx(ChatLayout.Messages, { children: messages.length === 0 ? (_jsx(OpenFilesButton, { children: ({ openFiles }) => (_jsx("div", { className: "flex flex-1 items-center px-4", children: _jsx(ChatEmptyState, { title: agentName, description: agentDescription, suggestedPrompts: suggestedPrompts, onPromptClick: (prompt) => {
|
|
223
|
+
return (_jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [_jsx(SidebarHotkey, {}), _jsxs(ChatLayout.Main, { children: [_jsx(AppChatHeader, { agentName: agentName, todos: todos, sources: sources, showHeader: messages.length > 0, sessionId: sessionId, tools: agentTools, mcps: agentMcps, subagents: agentSubagents, ...(debuggerUrl && { debuggerUrl }) }), connectionStatus === "error" && error && (_jsx("div", { className: "border-b border-destructive/20 bg-destructive/10 px-6 py-4", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "mb-1 text-paragraph-sm font-semibold text-destructive", children: "Connection Error" }), _jsx("p", { className: "whitespace-pre-line text-paragraph-sm text-foreground", children: error })] }), _jsx("button", { type: "button", onClick: connect, className: "rounded-lg bg-destructive px-4 py-2 text-paragraph-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive-hover", children: "Retry" })] }) })), _jsxs(ChatLayout.Body, { children: [_jsx(ChatLayout.Messages, { children: messages.length === 0 ? (_jsx(OpenFilesButton, { children: ({ openFiles, openSettings }) => (_jsx("div", { className: "flex flex-1 items-center px-4", children: _jsx(ChatEmptyState, { title: agentName, description: agentDescription, suggestedPrompts: suggestedPrompts, onPromptClick: (prompt) => {
|
|
194
224
|
sendMessage(prompt);
|
|
195
225
|
setPlaceholder("Type a message or / for commands...");
|
|
196
226
|
logger.info("Prompt clicked", { prompt });
|
|
197
|
-
}, onPromptHover: handlePromptHover, onPromptLeave: handlePromptLeave, onOpenFiles: openFiles
|
|
227
|
+
}, onPromptHover: handlePromptHover, onPromptLeave: handlePromptLeave, onOpenFiles: openFiles, onOpenSettings: openSettings, toolsAndMcpsCount: agentTools.length +
|
|
228
|
+
agentMcps.length +
|
|
229
|
+
agentSubagents.length }) })) })) : (_jsx("div", { className: "flex flex-col px-4", children: messages.map((message, index) => {
|
|
198
230
|
// Calculate dynamic spacing based on message sequence
|
|
199
231
|
const isFirst = index === 0;
|
|
200
232
|
const previousMessage = isFirst ? null : messages[index - 1];
|
|
@@ -213,5 +245,5 @@ export function ChatView({ client, initialSessionId, error: initError, }) {
|
|
|
213
245
|
previousMessage?.role === "assistant" ? "mt-2" : "mt-6";
|
|
214
246
|
}
|
|
215
247
|
return (_jsx(Message, { message: message, className: spacingClass, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id));
|
|
216
|
-
}) })) }), _jsx(ChatLayout.Footer, { children:
|
|
248
|
+
}) })) }), _jsx(ChatLayout.Footer, { children: _jsx(ChatInputWithAttachments, { client: client, startSession: startSession, placeholder: placeholder, latestContextSize: latestContextSize, currentModel: currentModel, commandMenuItems: commandMenuItems }) })] })] }), isLargeScreen && (_jsx(ChatLayout.Aside, { breakpoint: "lg", children: _jsx(AsideTabs, { todos: todos, tools: agentTools, mcps: agentMcps, subagents: agentSubagents }) }))] }));
|
|
217
249
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
|
|
2
|
+
export interface InvokingGroupProps {
|
|
3
|
+
toolCalls: ToolCallType[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* InvokingGroup component - displays a group of preliminary (invoking) tool calls
|
|
7
|
+
* Shows as "Invoking parallel operation (N)" with a summary of unique tool names
|
|
8
|
+
*/
|
|
9
|
+
export declare function InvokingGroup({ toolCalls }: InvokingGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ListVideo } from "lucide-react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
/**
|
|
5
|
+
* InvokingGroup component - displays a group of preliminary (invoking) tool calls
|
|
6
|
+
* Shows as "Invoking parallel operation (N)" with a summary of unique tool names
|
|
7
|
+
*/
|
|
8
|
+
export function InvokingGroup({ toolCalls }) {
|
|
9
|
+
// Get unique display names for the summary
|
|
10
|
+
const displayNames = toolCalls.map((tc) => tc.prettyName || tc.title);
|
|
11
|
+
const uniqueNames = [...new Set(displayNames)];
|
|
12
|
+
const summary = uniqueNames.length <= 2
|
|
13
|
+
? uniqueNames.join(", ")
|
|
14
|
+
: `${uniqueNames.slice(0, 2).join(", ")} +${uniqueNames.length - 2} more`;
|
|
15
|
+
return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("div", { className: "flex items-center gap-1.5 text-paragraph-sm text-muted-foreground/50", children: [_jsx(ListVideo, { className: "h-3 w-3" }), _jsx("span", { children: "Invoking parallel operation" }), _jsx("span", { className: "text-[10px] bg-muted px-1.5 py-0.5 rounded text-muted-foreground/50", children: toolCalls.length })] }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground/50 pl-4.5", children: summary })] }));
|
|
16
|
+
}
|
|
@@ -3,9 +3,11 @@ import { cva } from "class-variance-authority";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { useChatStore } from "../../core/store/chat-store.js";
|
|
5
5
|
import { cn } from "../lib/utils.js";
|
|
6
|
+
import { InvokingGroup } from "./InvokingGroup.js";
|
|
6
7
|
import { Reasoning } from "./Reasoning.js";
|
|
7
8
|
import { Response } from "./Response.js";
|
|
8
9
|
import { ToolCall } from "./ToolCall.js";
|
|
10
|
+
import { ToolCallGroup } from "./ToolCallGroup.js";
|
|
9
11
|
/**
|
|
10
12
|
* MessageContent component inspired by shadcn.io/ai
|
|
11
13
|
* Provides the content container with role-based styling
|
|
@@ -53,36 +55,150 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
53
55
|
.slice()
|
|
54
56
|
.sort((a, b) => (a.contentPosition ?? Infinity) -
|
|
55
57
|
(b.contentPosition ?? Infinity));
|
|
58
|
+
// Helper to check if a tool call is preliminary (invoking)
|
|
59
|
+
const isPreliminary = (tc) => tc.status === "pending" &&
|
|
60
|
+
(!tc.rawInput || Object.keys(tc.rawInput).length === 0);
|
|
61
|
+
// Helper to group tool calls by batchId and consecutive preliminary calls
|
|
62
|
+
const groupToolCalls = (toolCalls) => {
|
|
63
|
+
const result = [];
|
|
64
|
+
const batchGroups = new Map();
|
|
65
|
+
let currentInvokingGroup = [];
|
|
66
|
+
const flushInvokingGroup = () => {
|
|
67
|
+
if (currentInvokingGroup.length > 1) {
|
|
68
|
+
result.push({
|
|
69
|
+
type: "invoking",
|
|
70
|
+
toolCalls: currentInvokingGroup,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
else if (currentInvokingGroup.length === 1) {
|
|
74
|
+
result.push(currentInvokingGroup[0]);
|
|
75
|
+
}
|
|
76
|
+
currentInvokingGroup = [];
|
|
77
|
+
};
|
|
78
|
+
for (const tc of toolCalls) {
|
|
79
|
+
// Handle batch groups
|
|
80
|
+
if (tc.batchId) {
|
|
81
|
+
flushInvokingGroup();
|
|
82
|
+
const existing = batchGroups.get(tc.batchId);
|
|
83
|
+
if (existing) {
|
|
84
|
+
existing.push(tc);
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
const group = [tc];
|
|
88
|
+
batchGroups.set(tc.batchId, group);
|
|
89
|
+
result.push({ type: "batch", toolCalls: group });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Handle consecutive preliminary (invoking) tool calls
|
|
93
|
+
else if (isPreliminary(tc)) {
|
|
94
|
+
currentInvokingGroup.push(tc);
|
|
95
|
+
}
|
|
96
|
+
// Regular tool call
|
|
97
|
+
else {
|
|
98
|
+
flushInvokingGroup();
|
|
99
|
+
result.push(tc);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Flush any remaining invoking group
|
|
103
|
+
flushInvokingGroup();
|
|
104
|
+
return result;
|
|
105
|
+
};
|
|
106
|
+
// Helper to render a tool call or group
|
|
107
|
+
const renderToolCallOrGroup = (item, index) => {
|
|
108
|
+
// Batch group (parallel operations)
|
|
109
|
+
if (typeof item === "object" &&
|
|
110
|
+
"type" in item &&
|
|
111
|
+
item.type === "batch") {
|
|
112
|
+
return (_jsx(ToolCallGroup, { toolCalls: item.toolCalls }, `batch-${item.toolCalls[0]?.batchId || index}`));
|
|
113
|
+
}
|
|
114
|
+
// Invoking group (consecutive preliminary tool calls)
|
|
115
|
+
if (typeof item === "object" &&
|
|
116
|
+
"type" in item &&
|
|
117
|
+
item.type === "invoking") {
|
|
118
|
+
return (_jsx(InvokingGroup, { toolCalls: item.toolCalls }, `invoking-${item.toolCalls[0]?.id || index}`));
|
|
119
|
+
}
|
|
120
|
+
// Single tool call
|
|
121
|
+
return (_jsx(ToolCall, { toolCall: item }, item.id));
|
|
122
|
+
};
|
|
56
123
|
// If no tool calls or they don't have positions, render old way
|
|
57
124
|
if (sortedToolCalls.length === 0 ||
|
|
58
125
|
!sortedToolCalls.some((tc) => tc.contentPosition !== undefined)) {
|
|
59
|
-
|
|
126
|
+
const groupedToolCalls = groupToolCalls(sortedToolCalls);
|
|
127
|
+
return (_jsxs(_Fragment, { children: [groupedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) })), _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false })] }));
|
|
60
128
|
}
|
|
61
129
|
// Render content interleaved with tool calls
|
|
130
|
+
// Group consecutive tool calls with the same batchId
|
|
62
131
|
const elements = [];
|
|
63
132
|
let currentPosition = 0;
|
|
64
|
-
|
|
133
|
+
let currentBatch = [];
|
|
134
|
+
let currentBatchId;
|
|
135
|
+
const flushBatch = () => {
|
|
136
|
+
if (currentBatch.length > 1 && currentBatchId) {
|
|
137
|
+
elements.push(_jsx(ToolCallGroup, { toolCalls: currentBatch }, `group-${currentBatchId}`));
|
|
138
|
+
}
|
|
139
|
+
else if (currentBatch.length === 1) {
|
|
140
|
+
elements.push(_jsx("div", { children: _jsx(ToolCall, { toolCall: currentBatch[0] }) }, `tool-${currentBatch[0].id}`));
|
|
141
|
+
}
|
|
142
|
+
currentBatch = [];
|
|
143
|
+
currentBatchId = undefined;
|
|
144
|
+
};
|
|
145
|
+
// Separate preliminary tool calls - they should render at the end, not break text
|
|
146
|
+
const preliminaryToolCalls = sortedToolCalls.filter(isPreliminary);
|
|
147
|
+
const nonPreliminaryToolCalls = sortedToolCalls.filter((tc) => !isPreliminary(tc));
|
|
148
|
+
// Process non-preliminary tool calls inline with text
|
|
149
|
+
nonPreliminaryToolCalls.forEach((toolCall, index) => {
|
|
65
150
|
const position = toolCall.contentPosition ?? message.content.length;
|
|
66
151
|
// Add text before this tool call
|
|
67
152
|
if (position > currentPosition) {
|
|
153
|
+
// Flush any pending batch before adding text
|
|
154
|
+
flushBatch();
|
|
68
155
|
const textChunk = message.content.slice(currentPosition, position);
|
|
69
156
|
if (textChunk) {
|
|
70
157
|
elements.push(_jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }, `text-before-${toolCall.id}`));
|
|
71
158
|
}
|
|
72
159
|
}
|
|
73
|
-
//
|
|
74
|
-
|
|
160
|
+
// Check if this tool call should be batched
|
|
161
|
+
if (toolCall.batchId) {
|
|
162
|
+
if (currentBatchId === toolCall.batchId) {
|
|
163
|
+
// Same batch, add to current batch
|
|
164
|
+
currentBatch.push(toolCall);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// Different batch, flush previous and start new
|
|
168
|
+
flushBatch();
|
|
169
|
+
currentBatchId = toolCall.batchId;
|
|
170
|
+
currentBatch = [toolCall];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
// No batch ID, flush previous and render individually
|
|
175
|
+
flushBatch();
|
|
176
|
+
elements.push(_jsx("div", { children: _jsx(ToolCall, { toolCall: toolCall }) }, `tool-${toolCall.id}`));
|
|
177
|
+
}
|
|
75
178
|
currentPosition = position;
|
|
179
|
+
// Flush batch at the end
|
|
180
|
+
if (index === nonPreliminaryToolCalls.length - 1) {
|
|
181
|
+
flushBatch();
|
|
182
|
+
}
|
|
76
183
|
});
|
|
77
|
-
// Add remaining text after the last tool call
|
|
184
|
+
// Add remaining text after the last non-preliminary tool call
|
|
78
185
|
if (currentPosition < message.content.length) {
|
|
79
186
|
const remainingText = message.content.slice(currentPosition);
|
|
80
187
|
if (remainingText) {
|
|
81
188
|
elements.push(_jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }, "text-end"));
|
|
82
189
|
}
|
|
83
190
|
}
|
|
191
|
+
// Render preliminary (invoking) tool calls at the end, grouped
|
|
192
|
+
if (preliminaryToolCalls.length > 0) {
|
|
193
|
+
if (preliminaryToolCalls.length > 1) {
|
|
194
|
+
elements.push(_jsx(InvokingGroup, { toolCalls: preliminaryToolCalls }, `invoking-group-${preliminaryToolCalls[0].id}`));
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
elements.push(_jsx("div", { children: _jsx(ToolCall, { toolCall: preliminaryToolCalls[0] }) }, `tool-${preliminaryToolCalls[0].id}`));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
84
200
|
return _jsx(_Fragment, { children: elements });
|
|
85
|
-
})()) : (_jsx("div", { className: "whitespace-pre-wrap", children: message.content }))] }));
|
|
201
|
+
})()) : (_jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-2", children: message.images.map((image, index) => (_jsx("img", { src: `data:${image.mimeType};base64,${image.data}`, alt: `Attachment ${index + 1}`, className: "max-w-[200px] max-h-[200px] rounded-lg object-cover" }, index))) })), message.content && (_jsx("div", { className: "whitespace-pre-wrap", children: message.content }))] }))] }));
|
|
86
202
|
}
|
|
87
203
|
return (_jsx("div", { ref: ref, className: cn(messageContentVariants({ role, variant }), isStreaming && "animate-pulse-subtle", className), ...props, children: content }));
|
|
88
204
|
});
|
|
@@ -8,7 +8,7 @@ export interface PanelTabsHeaderProps extends Omit<React.ComponentPropsWithoutRe
|
|
|
8
8
|
/**
|
|
9
9
|
* Which tabs to show
|
|
10
10
|
*/
|
|
11
|
-
visibleTabs?: ("todo" | "files" | "database" | "sources")[];
|
|
11
|
+
visibleTabs?: ("todo" | "files" | "database" | "sources" | "settings")[];
|
|
12
12
|
/**
|
|
13
13
|
* Styling variant
|
|
14
14
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { CheckSquare, Database, FileText, Link2 } from "lucide-react";
|
|
2
|
+
import { CheckSquare, Database, FileText, Link2, Settings } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
5
|
import { TabsList, TabsTrigger } from "./Tabs.js";
|
|
@@ -25,6 +25,11 @@ export const PanelTabsHeader = React.forwardRef(({ showIcons = true, visibleTabs
|
|
|
25
25
|
label: "Sources",
|
|
26
26
|
icon: Link2,
|
|
27
27
|
},
|
|
28
|
+
{
|
|
29
|
+
id: "settings",
|
|
30
|
+
label: "Settings",
|
|
31
|
+
icon: Settings,
|
|
32
|
+
},
|
|
28
33
|
];
|
|
29
34
|
const tabs = allTabs.filter((tab) => visibleTabs.includes(tab.id));
|
|
30
35
|
const gap = variant === "compact" ? "gap-[4px]" : "gap-3";
|
|
@@ -91,6 +91,8 @@ export const Response = React.forwardRef(({ content, isStreaming = false, showEm
|
|
|
91
91
|
blockquote: ({ node, ...props }) => (_jsx("blockquote", { className: "border-l-4 border-[primary] pl-4 italic my-4 text-foreground bg-card py-2 rounded-r-md shadow-sm", ...props })),
|
|
92
92
|
// Horizontal rule
|
|
93
93
|
hr: ({ node, ...props }) => (_jsx("hr", { className: "my-6 border-t border-border opacity-50", ...props })),
|
|
94
|
+
// Image styling
|
|
95
|
+
img: ({ node, ...props }) => (_jsx("img", { className: "max-w-full h-auto rounded-md border border-border my-4", loading: "lazy", ...props })),
|
|
94
96
|
};
|
|
95
97
|
return (_jsx("div", { ref: ref, className: cn("markdown-content prose prose-sm max-w-none dark:prose-invert", className), ...props, children: _jsx(ReactMarkdown, { remarkPlugins: [remarkGfm], components: components, children: content }) }));
|
|
96
98
|
});
|
|
@@ -30,8 +30,8 @@ export const Task = React.forwardRef(({ task, collapsible = true, defaultExpande
|
|
|
30
30
|
};
|
|
31
31
|
return (_jsxs("div", { ref: ref, className: cn("rounded-lg border border-border bg-card transition-all", "hover:shadow-sm hover:border-border/80", className), ...props, children: [_jsxs("button", { type: "button", onClick: handleClick, className: cn("w-full flex items-center gap-3 px-3 py-2.5 text-left", hasDetails && collapsible && "cursor-pointer"), disabled: !hasDetails && !collapsible, children: [_jsx(StatusIcon, { className: cn("w-4 h-4 shrink-0", getStatusColor(), task.status === "in_progress" && "animate-spin") }), _jsx("span", { className: cn("flex-1 text-paragraph-sm font-[var(--font-family)]", task.status === "completed" && "line-through opacity-60", task.status === "in_progress" && "font-medium"), children: task.text }), hasDetails && collapsible && (_jsx(ChevronDown, { className: cn("w-4 h-4 text-foreground opacity-50 transition-transform duration-200 shrink-0", isExpanded && "rotate-180"), "aria-hidden": "true" }))] }), hasDetails && isExpanded && (_jsxs("div", { className: "px-3 pb-3 pt-1 border-t border-border/50 animate-fadeIn", children: [task.details && (_jsx("p", { className: "text-paragraph-sm text-foreground opacity-80 leading-relaxed mb-2", children: task.details })), task.files && task.files.length > 0 && (_jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "text-caption font-medium text-foreground opacity-60 uppercase tracking-wide", children: "Files:" }), _jsx("div", { className: "space-y-1", children: task.files.map((file) => (_jsx("div", { className: "text-caption font-mono text-foreground opacity-70 bg-background px-2 py-1 rounded border border-border/50", children: file }, file))) })] }))] }))] }));
|
|
32
32
|
});
|
|
33
|
-
Task.displayName = "
|
|
34
|
-
export const TaskList = React.forwardRef(({ tasks, collapsible = true, onTaskClick, emptyMessage = "No
|
|
33
|
+
Task.displayName = "Subagent";
|
|
34
|
+
export const TaskList = React.forwardRef(({ tasks, collapsible = true, onTaskClick, emptyMessage = "No subagents yet.", className, ...props }, ref) => {
|
|
35
35
|
return (_jsx("div", { ref: ref, className: cn("space-y-2 max-h-96 overflow-y-auto", className), ...props, children: tasks.length === 0 ? (_jsx("p", { className: "text-paragraph-sm text-foreground opacity-60 italic py-4 text-center", children: emptyMessage })) : (tasks.map((task) => (_jsx(Task, { task: task, collapsible: collapsible, ...(onTaskClick ? { onTaskClick } : {}) }, task.id)))) }));
|
|
36
36
|
});
|
|
37
|
-
TaskList.displayName = "
|
|
37
|
+
TaskList.displayName = "SubagentList";
|
|
@@ -15,7 +15,7 @@ export const TodoListItem = React.forwardRef(({ todo, className, ...props }, ref
|
|
|
15
15
|
// Focus state - matching FileSystemItem
|
|
16
16
|
"focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-border-dark",
|
|
17
17
|
// Selected state (if needed later)
|
|
18
|
-
isSelected && "bg-accent", className), role: "button", tabIndex: 0, ...props, children: [_jsx("div", { className: "shrink-0 size-4 flex items-center justify-center text-foreground", children:
|
|
18
|
+
isSelected && "bg-accent", className), role: "button", tabIndex: 0, ...props, children: [_jsx("div", { className: "shrink-0 size-4 flex items-center justify-center text-foreground", children: todo.status === "completed" ? (_jsx(CircleCheck, { className: "size-4 text-muted-foreground" })) : todo.status === "in_progress" ? (_jsx("div", { className: "size-2.5 rounded-full bg-foreground animate-pulse-scale" })) : (_jsx(Circle, { className: "size-4 text-foreground" })) }), _jsx("p", { className: cn("flex-1 text-foreground",
|
|
19
19
|
// Completed state: strikethrough + muted color
|
|
20
20
|
isCompleted && "line-through text-muted-foreground",
|
|
21
21
|
// In-progress state: medium weight for emphasis
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import JsonView from "@uiw/react-json-view";
|
|
3
|
-
import { CheckSquare, ChevronDown, Cloud, Edit, FileText, Globe, Link, Search, Wrench, } from "lucide-react";
|
|
4
|
-
import { useState } from "react";
|
|
3
|
+
import { AlertCircle, BrainCircuit, CheckSquare, ChevronDown, ChevronRight, Cloud, Edit, FileText, Globe, Image, Link, Search, Wrench, } from "lucide-react";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import { ChatLayout } from "./index.js";
|
|
5
6
|
import { useTheme } from "./ThemeProvider.js";
|
|
6
7
|
/**
|
|
7
8
|
* Map of icon names to Lucide components
|
|
8
9
|
*/
|
|
9
10
|
const ICON_MAP = {
|
|
10
11
|
Globe: Globe,
|
|
12
|
+
Image: Image,
|
|
11
13
|
Link: Link,
|
|
12
14
|
Cloud: Cloud,
|
|
13
15
|
CheckSquare: CheckSquare,
|
|
@@ -15,6 +17,7 @@ const ICON_MAP = {
|
|
|
15
17
|
FileText: FileText,
|
|
16
18
|
Edit: Edit,
|
|
17
19
|
Wrench: Wrench,
|
|
20
|
+
BrainCircuit: BrainCircuit,
|
|
18
21
|
};
|
|
19
22
|
/**
|
|
20
23
|
* Tool call kind icons (using emoji for simplicity)
|
|
@@ -37,12 +40,39 @@ const _kindIcons = {
|
|
|
37
40
|
export function ToolCall({ toolCall }) {
|
|
38
41
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
39
42
|
const { resolvedTheme } = useTheme();
|
|
43
|
+
// Detect TodoWrite tool
|
|
44
|
+
const isTodoWrite = toolCall.title === "todo_write";
|
|
45
|
+
// Safely access ChatLayout context - will be undefined if not within ChatLayout
|
|
46
|
+
const layoutContext = React.useContext(ChatLayout.Context);
|
|
47
|
+
// Click handler: toggle sidepanel for TodoWrite, expand for others
|
|
48
|
+
const handleHeaderClick = React.useCallback(() => {
|
|
49
|
+
if (isTodoWrite && layoutContext) {
|
|
50
|
+
// Toggle sidepanel - close if already open on todo tab, otherwise open
|
|
51
|
+
if (layoutContext.panelSize !== "hidden" &&
|
|
52
|
+
layoutContext.activeTab === "todo") {
|
|
53
|
+
layoutContext.setPanelSize("hidden");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
layoutContext.setPanelSize("small");
|
|
57
|
+
layoutContext.setActiveTab("todo");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
// Normal expand/collapse
|
|
62
|
+
setIsExpanded(!isExpanded);
|
|
63
|
+
}
|
|
64
|
+
}, [isTodoWrite, layoutContext, isExpanded]);
|
|
40
65
|
// Determine which icon to show
|
|
41
66
|
const IconComponent = toolCall.icon && ICON_MAP[toolCall.icon]
|
|
42
67
|
? ICON_MAP[toolCall.icon]
|
|
43
|
-
:
|
|
68
|
+
: BrainCircuit;
|
|
44
69
|
// Determine display name
|
|
45
70
|
const displayName = toolCall.prettyName || toolCall.title;
|
|
71
|
+
// Check if there's an error
|
|
72
|
+
const hasError = toolCall.status === "failed" || !!toolCall.error;
|
|
73
|
+
// Check if this is a preliminary (pending) tool call without full details yet
|
|
74
|
+
const isPreliminary = toolCall.status === "pending" &&
|
|
75
|
+
(!toolCall.rawInput || Object.keys(toolCall.rawInput).length === 0);
|
|
46
76
|
// JSON View style based on theme
|
|
47
77
|
const jsonStyle = {
|
|
48
78
|
fontSize: "11px",
|
|
@@ -74,7 +104,11 @@ export function ToolCall({ toolCall }) {
|
|
|
74
104
|
"--w-rjv-type-nan-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
|
|
75
105
|
"--w-rjv-type-undefined-color": resolvedTheme === "dark" ? "#ef4444" : "#dc2626",
|
|
76
106
|
};
|
|
77
|
-
|
|
107
|
+
// Preliminary tool calls show as simple light gray text without expansion
|
|
108
|
+
if (isPreliminary) {
|
|
109
|
+
return (_jsx("div", { className: "flex flex-col my-4", children: _jsxs("span", { className: "text-paragraph-sm text-muted-foreground/50", children: ["Invoking ", displayName] }) }));
|
|
110
|
+
}
|
|
111
|
+
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: "text-muted-foreground", 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 && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [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 &&
|
|
78
112
|
loc.line !== undefined &&
|
|
79
113
|
`:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), toolCall.rawInput && Object.keys(toolCall.rawInput).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: "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.content && toolCall.content.length > 0) ||
|
|
80
114
|
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) => {
|
|
@@ -122,6 +156,24 @@ export function ToolCall({ toolCall }) {
|
|
|
122
156
|
if (block.type === "text" && "text" in block) {
|
|
123
157
|
return renderTextContent(block.text, getBlockKey());
|
|
124
158
|
}
|
|
159
|
+
// Handle image blocks
|
|
160
|
+
if (block.type === "image") {
|
|
161
|
+
const alt = block.alt || "Generated image";
|
|
162
|
+
let imageSrc;
|
|
163
|
+
if ("data" in block) {
|
|
164
|
+
// Base64 encoded image
|
|
165
|
+
const mimeType = block.mimeType || "image/png";
|
|
166
|
+
imageSrc = `data:${mimeType};base64,${block.data}`;
|
|
167
|
+
}
|
|
168
|
+
else if ("url" in block) {
|
|
169
|
+
// URL or file path
|
|
170
|
+
imageSrc = block.url;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
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()));
|
|
176
|
+
}
|
|
125
177
|
// Handle diff blocks
|
|
126
178
|
if (block.type === "diff" &&
|
|
127
179
|
"path" in block &&
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ToolCall as ToolCallType } from "../../core/schemas/tool-call.js";
|
|
2
|
+
export interface ToolCallGroupProps {
|
|
3
|
+
toolCalls: ToolCallType[];
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* ToolCallGroup component - displays a group of parallel tool calls with collapsible details
|
|
7
|
+
*/
|
|
8
|
+
export declare function ToolCallGroup({ toolCalls }: ToolCallGroupProps): import("react/jsx-runtime").JSX.Element;
|