@townco/ui 0.1.68 → 0.1.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/hooks/use-chat-messages.d.ts +6 -1
- package/dist/core/hooks/use-chat-session.d.ts +1 -1
- package/dist/core/hooks/use-tool-calls.d.ts +6 -1
- package/dist/core/schemas/chat.d.ts +10 -0
- package/dist/core/schemas/tool-call.d.ts +5 -0
- package/dist/core/schemas/tool-call.js +8 -0
- package/dist/core/utils/tool-call-state.d.ts +30 -0
- package/dist/core/utils/tool-call-state.js +73 -0
- package/dist/core/utils/tool-summary.d.ts +13 -0
- package/dist/core/utils/tool-summary.js +172 -0
- package/dist/core/utils/tool-verbiage.d.ts +28 -0
- package/dist/core/utils/tool-verbiage.js +185 -0
- package/dist/gui/components/AppSidebar.d.ts +22 -0
- package/dist/gui/components/AppSidebar.js +22 -0
- package/dist/gui/components/Button.d.ts +1 -1
- package/dist/gui/components/ChatLayout.d.ts +5 -0
- package/dist/gui/components/ChatLayout.js +239 -132
- package/dist/gui/components/ChatView.js +42 -118
- package/dist/gui/components/MessageContent.js +151 -39
- package/dist/gui/components/SessionHistory.d.ts +10 -0
- package/dist/gui/components/SessionHistory.js +101 -0
- package/dist/gui/components/SessionHistoryItem.d.ts +11 -0
- package/dist/gui/components/SessionHistoryItem.js +24 -0
- package/dist/gui/components/Sheet.d.ts +25 -0
- package/dist/gui/components/Sheet.js +36 -0
- package/dist/gui/components/Sidebar.d.ts +65 -0
- package/dist/gui/components/Sidebar.js +231 -0
- package/dist/gui/components/SidebarToggle.d.ts +3 -0
- package/dist/gui/components/SidebarToggle.js +9 -0
- package/dist/gui/components/ToolCallList.js +3 -3
- package/dist/gui/components/ToolOperation.d.ts +11 -0
- package/dist/gui/components/ToolOperation.js +289 -0
- package/dist/gui/components/WorkProgress.d.ts +20 -0
- package/dist/gui/components/WorkProgress.js +79 -0
- package/dist/gui/components/index.d.ts +8 -1
- package/dist/gui/components/index.js +9 -1
- package/dist/gui/hooks/index.d.ts +1 -0
- package/dist/gui/hooks/index.js +1 -0
- package/dist/gui/hooks/use-mobile.d.ts +1 -0
- package/dist/gui/hooks/use-mobile.js +15 -0
- package/dist/gui/index.d.ts +1 -0
- package/dist/gui/index.js +2 -0
- package/dist/gui/lib/motion.d.ts +55 -0
- package/dist/gui/lib/motion.js +217 -0
- package/dist/sdk/schemas/session.d.ts +11 -6
- package/dist/sdk/transports/types.d.ts +5 -0
- package/package.json +8 -7
- package/src/styles/global.css +128 -1
- package/dist/gui/components/InvokingGroup.d.ts +0 -9
- package/dist/gui/components/InvokingGroup.js +0 -16
- package/dist/gui/components/SubagentStream.d.ts +0 -23
- package/dist/gui/components/SubagentStream.js +0 -98
- package/dist/gui/components/ToolCall.d.ts +0 -8
- package/dist/gui/components/ToolCall.js +0 -234
- package/dist/gui/components/ToolCallGroup.d.ts +0 -8
- package/dist/gui/components/ToolCallGroup.js +0 -29
|
@@ -1,12 +1,11 @@
|
|
|
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, Bug,
|
|
4
|
-
import {
|
|
3
|
+
import { ArrowUp, Bug, ChevronUp, PanelRight, Settings, X } from "lucide-react";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
5
|
import { useChatMessages, useChatSession, useToolCalls, } from "../../core/hooks/index.js";
|
|
6
6
|
import { selectTodosForCurrentSession, useChatStore, } from "../../core/store/chat-store.js";
|
|
7
|
-
import { calculateTokenPercentage, formatTokenPercentage, } from "../../core/utils/model-context.js";
|
|
8
7
|
import { cn } from "../lib/utils.js";
|
|
9
|
-
import { ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton,
|
|
8
|
+
import { AppSidebar, ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, FilesTabContent, IconButton, Message, MessageContent, PanelTabsHeader, SettingsTabContent, SidebarInset, SidebarProvider, SidebarToggle, SourcesTabContent, Tabs, TabsContent, ThemeToggle, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./index.js";
|
|
10
9
|
const logger = createLogger("gui");
|
|
11
10
|
// Helper component to provide openFiles callback
|
|
12
11
|
function OpenFilesButton({ children, }) {
|
|
@@ -31,85 +30,8 @@ function OpenFilesButton({ children, }) {
|
|
|
31
30
|
};
|
|
32
31
|
return _jsx(_Fragment, { children: children({ openFiles, openSettings }) });
|
|
33
32
|
}
|
|
34
|
-
//
|
|
35
|
-
|
|
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
|
-
// Format relative time from date string
|
|
51
|
-
function formatRelativeTime(dateString) {
|
|
52
|
-
const date = new Date(dateString);
|
|
53
|
-
const now = new Date();
|
|
54
|
-
const diffMs = now.getTime() - date.getTime();
|
|
55
|
-
const diffMins = Math.floor(diffMs / 60000);
|
|
56
|
-
const diffHours = Math.floor(diffMs / 3600000);
|
|
57
|
-
const diffDays = Math.floor(diffMs / 86400000);
|
|
58
|
-
if (diffMins < 1)
|
|
59
|
-
return "Just now";
|
|
60
|
-
if (diffMins < 60)
|
|
61
|
-
return `${diffMins}m ago`;
|
|
62
|
-
if (diffHours < 24)
|
|
63
|
-
return `${diffHours}h ago`;
|
|
64
|
-
if (diffDays < 7)
|
|
65
|
-
return `${diffDays}d ago`;
|
|
66
|
-
return date.toLocaleDateString();
|
|
67
|
-
}
|
|
68
|
-
// Session switcher dropdown component
|
|
69
|
-
function SessionSwitcher({ agentName, client, currentSessionId, }) {
|
|
70
|
-
const [sessions, setSessions] = useState([]);
|
|
71
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
72
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
73
|
-
const fetchSessions = useCallback(async () => {
|
|
74
|
-
if (!client)
|
|
75
|
-
return;
|
|
76
|
-
setIsLoading(true);
|
|
77
|
-
try {
|
|
78
|
-
const sessionList = await client.listSessions();
|
|
79
|
-
setSessions(sessionList);
|
|
80
|
-
}
|
|
81
|
-
catch (error) {
|
|
82
|
-
logger.error("Failed to fetch sessions", { error });
|
|
83
|
-
}
|
|
84
|
-
finally {
|
|
85
|
-
setIsLoading(false);
|
|
86
|
-
}
|
|
87
|
-
}, [client]);
|
|
88
|
-
// Fetch sessions when dropdown opens
|
|
89
|
-
useEffect(() => {
|
|
90
|
-
if (isOpen) {
|
|
91
|
-
fetchSessions();
|
|
92
|
-
}
|
|
93
|
-
}, [isOpen, fetchSessions]);
|
|
94
|
-
const handleNewSession = () => {
|
|
95
|
-
// Clear session from URL and reload to start fresh
|
|
96
|
-
const url = new URL(window.location.href);
|
|
97
|
-
url.searchParams.delete("session");
|
|
98
|
-
window.location.href = url.toString();
|
|
99
|
-
};
|
|
100
|
-
const handleSessionSelect = (sessionId) => {
|
|
101
|
-
if (sessionId === currentSessionId) {
|
|
102
|
-
setIsOpen(false);
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
// Update URL with session ID and reload
|
|
106
|
-
const url = new URL(window.location.href);
|
|
107
|
-
url.searchParams.set("session", sessionId);
|
|
108
|
-
window.location.href = url.toString();
|
|
109
|
-
};
|
|
110
|
-
return (_jsxs(DropdownMenu, { open: isOpen, onOpenChange: setIsOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex items-center gap-1 text-heading-4 text-foreground hover:text-foreground/80 transition-colors cursor-pointer", children: [agentName, _jsx(ChevronDown, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isOpen && "rotate-180") })] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "w-72", children: [_jsxs(DropdownMenuLabel, { className: "flex items-center justify-between", children: [_jsx("span", { children: "Sessions" }), isLoading && (_jsx("span", { className: "text-caption text-muted-foreground", children: "Loading..." }))] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: handleNewSession, className: "gap-2", children: [_jsx(Plus, { className: "size-4" }), _jsx("span", { children: "New Session" })] }), sessions.length > 0 && _jsx(DropdownMenuSeparator, {}), _jsx("div", { className: "max-h-64 overflow-y-auto", children: sessions.map((session) => (_jsxs(DropdownMenuItem, { onClick: () => handleSessionSelect(session.sessionId), className: cn("flex flex-col items-start gap-0.5 py-2", session.sessionId === currentSessionId &&
|
|
111
|
-
"bg-muted/50 font-medium"), children: [_jsxs("div", { className: "flex w-full items-center justify-between gap-2", children: [_jsx("span", { className: "truncate text-paragraph-sm", children: session.firstUserMessage || "Empty session" }), session.sessionId === currentSessionId && (_jsx("span", { className: "shrink-0 text-caption text-primary", children: "Current" }))] }), _jsxs("span", { className: "text-caption text-muted-foreground", children: [formatRelativeTime(session.updatedAt), " \u2022 ", session.messageCount, " ", "messages"] })] }, session.sessionId))) }), sessions.length === 0 && !isLoading && (_jsx("div", { className: "px-2 py-4 text-center text-paragraph-sm text-muted-foreground", children: "No previous sessions" }))] })] }));
|
|
112
|
-
}
|
|
33
|
+
// Note: Keyboard shortcut (Cmd+B / Ctrl+B) for toggling the right panel
|
|
34
|
+
// is now handled internally by ChatLayout.Root
|
|
113
35
|
// Chat input with attachment handling
|
|
114
36
|
function ChatInputWithAttachments({ client, startSession, placeholder, latestContextSize, currentModel, commandMenuItems, }) {
|
|
115
37
|
const attachedFiles = useChatStore((state) => state.input.attachedFiles);
|
|
@@ -128,21 +50,19 @@ function AsideTabs({ todos, tools, mcps, subagents, }) {
|
|
|
128
50
|
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-4 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 ?? [] }) })] }));
|
|
129
51
|
}
|
|
130
52
|
// Mobile header component that uses ChatHeader context
|
|
131
|
-
function MobileHeader({ agentName, showHeader,
|
|
53
|
+
function MobileHeader({ agentName, showHeader, }) {
|
|
132
54
|
const { isExpanded, setIsExpanded } = ChatHeader.useChatHeaderContext();
|
|
133
|
-
return (_jsxs("div", { className: "flex lg:hidden items-center flex-1", children: [
|
|
55
|
+
return (_jsxs("div", { className: "flex lg:hidden items-center flex-1", children: [_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx(SidebarToggle, {}), showHeader && (_jsx("span", { className: "text-heading-4 text-foreground", children: agentName }))] }), _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") }) })] }));
|
|
134
56
|
}
|
|
135
57
|
// Header component that uses ChatLayout context (must be inside ChatLayout.Root)
|
|
136
|
-
function AppChatHeader({ agentName, todos, sources, showHeader, sessionId, debuggerUrl, tools, mcps, subagents,
|
|
137
|
-
const {
|
|
58
|
+
function AppChatHeader({ agentName, todos, sources, showHeader, sessionId, debuggerUrl, tools, mcps, subagents, }) {
|
|
59
|
+
const { togglePanel, panelOpen } = ChatLayout.useChatLayoutContext();
|
|
138
60
|
const debuggerLink = debuggerUrl
|
|
139
61
|
? sessionId
|
|
140
62
|
? `${debuggerUrl}/sessions/${sessionId}`
|
|
141
63
|
: debuggerUrl
|
|
142
64
|
: null;
|
|
143
|
-
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: [
|
|
144
|
-
setPanelSize(panelSize === "hidden" ? "small" : "hidden");
|
|
145
|
-
}, children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName, showHeader: showHeader, client: client, currentSessionId: sessionId }), _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 ?? [] }) })] }) })] }));
|
|
65
|
+
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: [_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx(SidebarToggle, {}), showHeader && (_jsx("span", { className: "text-heading-4 text-foreground", children: agentName }))] }), 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: sessionId ? "View session in debugger" : "Open debugger" }) })] }) })), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle panel", onClick: togglePanel, "data-state": panelOpen ? "open" : "closed", 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 ?? [] }) })] }) })] }));
|
|
146
66
|
}
|
|
147
67
|
export function ChatView({ client, initialSessionId, error: initError, debuggerUrl, }) {
|
|
148
68
|
// Use shared hooks from @townco/ui/core - MUST be called before any early returns
|
|
@@ -291,32 +211,36 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
|
|
|
291
211
|
},
|
|
292
212
|
},
|
|
293
213
|
];
|
|
294
|
-
return (_jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
214
|
+
return (_jsxs(SidebarProvider, { defaultOpen: false, children: [_jsx(AppSidebar, { client: client, currentSessionId: sessionId }), _jsx(SidebarInset, { children: _jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [_jsxs(ChatLayout.Main, { children: [!hideTopBar && (_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) => {
|
|
215
|
+
sendMessage(prompt);
|
|
216
|
+
setPlaceholder("Type a message or / for commands...");
|
|
217
|
+
logger.info("Prompt clicked", { prompt });
|
|
218
|
+
}, onPromptHover: handlePromptHover, onPromptLeave: handlePromptLeave, onOpenFiles: openFiles, onOpenSettings: openSettings, toolsAndMcpsCount: agentTools.length +
|
|
219
|
+
agentMcps.length +
|
|
220
|
+
agentSubagents.length }) })) })) : (_jsx("div", { className: "flex flex-col px-4", children: messages.map((message, index) => {
|
|
221
|
+
// Calculate dynamic spacing based on message sequence
|
|
222
|
+
const isFirst = index === 0;
|
|
223
|
+
const previousMessage = isFirst
|
|
224
|
+
? null
|
|
225
|
+
: messages[index - 1];
|
|
226
|
+
let spacingClass = "mt-2";
|
|
227
|
+
if (isFirst) {
|
|
228
|
+
// First message needs more top margin if it's an assistant initial message
|
|
229
|
+
spacingClass =
|
|
230
|
+
message.role === "assistant" ? "mt-8" : "mt-2";
|
|
231
|
+
}
|
|
232
|
+
else if (message.role === "user") {
|
|
233
|
+
// User message usually starts a new turn
|
|
234
|
+
spacingClass =
|
|
235
|
+
previousMessage?.role === "user" ? "mt-4" : "mt-4";
|
|
236
|
+
}
|
|
237
|
+
else if (message.role === "assistant") {
|
|
238
|
+
// Assistant message is usually a response
|
|
239
|
+
spacingClass =
|
|
240
|
+
previousMessage?.role === "assistant"
|
|
241
|
+
? "mt-2"
|
|
242
|
+
: "mt-6";
|
|
243
|
+
}
|
|
244
|
+
return (_jsx(Message, { message: message, className: spacingClass, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id));
|
|
245
|
+
}) })) }), _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 }) }))] }) })] }));
|
|
322
246
|
}
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { cva } from "class-variance-authority";
|
|
3
|
+
import { AnimatePresence, motion, useReducedMotion } from "framer-motion";
|
|
3
4
|
import * as React from "react";
|
|
4
5
|
import { useChatStore } from "../../core/store/chat-store.js";
|
|
6
|
+
import { isPreliminaryToolCall } from "../../core/utils/tool-call-state.js";
|
|
7
|
+
import { getDuration, getTransition, motionEasing, shimmerTransition, } from "../lib/motion.js";
|
|
5
8
|
import { cn } from "../lib/utils.js";
|
|
6
|
-
import { InvokingGroup } from "./InvokingGroup.js";
|
|
7
9
|
import { Reasoning } from "./Reasoning.js";
|
|
8
10
|
import { Response } from "./Response.js";
|
|
9
|
-
import {
|
|
10
|
-
import { ToolCallGroup } from "./ToolCallGroup.js";
|
|
11
|
+
import { ToolOperation } from "./ToolOperation.js";
|
|
11
12
|
/**
|
|
12
13
|
* MessageContent component inspired by shadcn.io/ai
|
|
13
14
|
* Provides the content container with role-based styling
|
|
@@ -35,6 +36,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
35
36
|
// Get streaming start time and current model from store
|
|
36
37
|
const streamingStartTime = useChatStore((state) => state.streamingStartTime);
|
|
37
38
|
const currentModel = useChatStore((state) => state.currentModel);
|
|
39
|
+
const shouldReduceMotion = useReducedMotion();
|
|
38
40
|
// Use smart rendering if message is provided and no custom children
|
|
39
41
|
const useSmartRendering = message && !children;
|
|
40
42
|
// Derive props from message if using smart rendering
|
|
@@ -49,33 +51,68 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
49
51
|
const hasThinking = !!thinking;
|
|
50
52
|
// Check if waiting (streaming but no content yet)
|
|
51
53
|
const isWaiting = message.isStreaming && !message.content && message.role === "assistant";
|
|
52
|
-
content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx(
|
|
54
|
+
content = (_jsxs(_Fragment, { children: [message.role === "assistant" && hasThinking && (_jsx(motion.div, { initial: {
|
|
55
|
+
filter: "blur(12px)",
|
|
56
|
+
opacity: 0,
|
|
57
|
+
y: 12,
|
|
58
|
+
}, animate: {
|
|
59
|
+
filter: "blur(0px)",
|
|
60
|
+
opacity: 1,
|
|
61
|
+
y: 0,
|
|
62
|
+
}, exit: {
|
|
63
|
+
filter: "blur(12px)",
|
|
64
|
+
opacity: 0,
|
|
65
|
+
y: -12,
|
|
66
|
+
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
67
|
+
duration: 0.5,
|
|
68
|
+
ease: motionEasing.smooth,
|
|
69
|
+
}), children: _jsx(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true }) })), isWaiting && streamingStartTime && (_jsx(motion.div, { initial: {
|
|
70
|
+
filter: "blur(12px)",
|
|
71
|
+
opacity: 0,
|
|
72
|
+
y: 12,
|
|
73
|
+
}, animate: {
|
|
74
|
+
filter: "blur(0px)",
|
|
75
|
+
opacity: 1,
|
|
76
|
+
y: 0,
|
|
77
|
+
}, exit: {
|
|
78
|
+
filter: "blur(12px)",
|
|
79
|
+
opacity: 0,
|
|
80
|
+
y: -12,
|
|
81
|
+
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
82
|
+
duration: 0.4,
|
|
83
|
+
ease: motionEasing.smooth,
|
|
84
|
+
}), children: _jsx(motion.div, { className: "flex flex-col my-4 rounded-md px-1 -mx-1 w-fit", animate: {
|
|
85
|
+
backgroundPosition: ["-200% 0", "200% 0"],
|
|
86
|
+
}, transition: {
|
|
87
|
+
...shimmerTransition,
|
|
88
|
+
duration: getDuration(shouldReduceMotion ?? false, 1.5),
|
|
89
|
+
}, style: {
|
|
90
|
+
backgroundImage: "linear-gradient(90deg, transparent 5%, rgba(255, 255, 255, 0.75) 25%, transparent 35%)",
|
|
91
|
+
backgroundSize: "200% 100%",
|
|
92
|
+
}, children: _jsx("div", { className: "flex items-center gap-1.5", children: _jsx("span", { className: "text-paragraph-sm text-text-secondary/70", children: "Thinking..." }) }) }) })), message.role === "assistant" ? ((() => {
|
|
53
93
|
// Sort tool calls by content position
|
|
54
94
|
const sortedToolCalls = (message.toolCalls || [])
|
|
55
95
|
.slice()
|
|
56
96
|
.sort((a, b) => (a.contentPosition ?? Infinity) -
|
|
57
97
|
(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
98
|
// Helper to group tool calls by batchId, consecutive same-title calls, or consecutive preliminary calls
|
|
62
99
|
const groupToolCalls = (toolCalls) => {
|
|
63
100
|
const result = [];
|
|
64
101
|
const batchGroups = new Map();
|
|
65
|
-
let
|
|
102
|
+
let currentSelectingGroup = [];
|
|
66
103
|
let currentConsecutiveGroup = [];
|
|
67
104
|
let currentConsecutiveTitle = null;
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
105
|
+
const flushSelectingGroup = () => {
|
|
106
|
+
if (currentSelectingGroup.length > 1) {
|
|
70
107
|
result.push({
|
|
71
|
-
type: "
|
|
72
|
-
toolCalls:
|
|
108
|
+
type: "selecting",
|
|
109
|
+
toolCalls: currentSelectingGroup,
|
|
73
110
|
});
|
|
74
111
|
}
|
|
75
|
-
else if (
|
|
76
|
-
result.push(
|
|
112
|
+
else if (currentSelectingGroup.length === 1) {
|
|
113
|
+
result.push(currentSelectingGroup[0]);
|
|
77
114
|
}
|
|
78
|
-
|
|
115
|
+
currentSelectingGroup = [];
|
|
79
116
|
};
|
|
80
117
|
const flushConsecutiveGroup = () => {
|
|
81
118
|
if (currentConsecutiveGroup.length > 1) {
|
|
@@ -94,7 +131,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
94
131
|
for (const tc of toolCalls) {
|
|
95
132
|
// Handle batch groups (explicit batchId)
|
|
96
133
|
if (tc.batchId) {
|
|
97
|
-
|
|
134
|
+
flushSelectingGroup();
|
|
98
135
|
flushConsecutiveGroup();
|
|
99
136
|
const existing = batchGroups.get(tc.batchId);
|
|
100
137
|
if (existing) {
|
|
@@ -106,14 +143,14 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
106
143
|
result.push({ type: "batch", toolCalls: group });
|
|
107
144
|
}
|
|
108
145
|
}
|
|
109
|
-
// Handle consecutive preliminary (
|
|
110
|
-
else if (
|
|
146
|
+
// Handle consecutive preliminary (selecting) tool calls
|
|
147
|
+
else if (isPreliminaryToolCall(tc)) {
|
|
111
148
|
flushConsecutiveGroup();
|
|
112
|
-
|
|
149
|
+
currentSelectingGroup.push(tc);
|
|
113
150
|
}
|
|
114
151
|
// Regular tool call - group consecutive same-title calls (e.g., subagent)
|
|
115
152
|
else {
|
|
116
|
-
|
|
153
|
+
flushSelectingGroup();
|
|
117
154
|
// Check if this continues a consecutive group
|
|
118
155
|
if (currentConsecutiveTitle === tc.title) {
|
|
119
156
|
currentConsecutiveGroup.push(tc);
|
|
@@ -127,7 +164,7 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
127
164
|
}
|
|
128
165
|
}
|
|
129
166
|
// Flush any remaining groups
|
|
130
|
-
|
|
167
|
+
flushSelectingGroup();
|
|
131
168
|
flushConsecutiveGroup();
|
|
132
169
|
return result;
|
|
133
170
|
};
|
|
@@ -137,22 +174,37 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
137
174
|
if (typeof item === "object" &&
|
|
138
175
|
"type" in item &&
|
|
139
176
|
item.type === "batch") {
|
|
140
|
-
return (_jsx(
|
|
177
|
+
return (_jsx(ToolOperation, { toolCalls: item.toolCalls, isGrouped: true }, `batch-${item.toolCalls[0]?.batchId || index}`));
|
|
141
178
|
}
|
|
142
|
-
//
|
|
179
|
+
// Selecting group (consecutive preliminary tool calls)
|
|
143
180
|
if (typeof item === "object" &&
|
|
144
181
|
"type" in item &&
|
|
145
|
-
item.type === "
|
|
146
|
-
return (_jsx(
|
|
182
|
+
item.type === "selecting") {
|
|
183
|
+
return (_jsx(ToolOperation, { toolCalls: item.toolCalls, isGrouped: true }, `selecting-${item.toolCalls[0]?.id || index}`));
|
|
147
184
|
}
|
|
148
185
|
// Single tool call
|
|
149
|
-
return (_jsx(
|
|
186
|
+
return (_jsx(ToolOperation, { toolCalls: [item], isGrouped: false }, item.id));
|
|
150
187
|
};
|
|
151
|
-
// If no tool calls or they don't have positions, render
|
|
188
|
+
// If no tool calls or they don't have positions, render simplified way
|
|
152
189
|
if (sortedToolCalls.length === 0 ||
|
|
153
190
|
!sortedToolCalls.some((tc) => tc.contentPosition !== undefined)) {
|
|
154
191
|
const groupedToolCalls = groupToolCalls(sortedToolCalls);
|
|
155
|
-
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(
|
|
192
|
+
return (_jsxs(_Fragment, { children: [groupedToolCalls.length > 0 && (_jsx(AnimatePresence, { mode: "popLayout", children: _jsx("div", { className: "flex flex-col gap-2 mb-1", children: groupedToolCalls.map((item, index) => renderToolCallOrGroup(item, index)) }) })), _jsx(motion.div, { initial: {
|
|
193
|
+
filter: "blur(12px)",
|
|
194
|
+
opacity: 0,
|
|
195
|
+
y: 12,
|
|
196
|
+
}, animate: {
|
|
197
|
+
filter: "blur(0px)",
|
|
198
|
+
opacity: 1,
|
|
199
|
+
y: 0,
|
|
200
|
+
}, exit: {
|
|
201
|
+
filter: "blur(12px)",
|
|
202
|
+
opacity: 0,
|
|
203
|
+
y: -12,
|
|
204
|
+
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
205
|
+
duration: 0.4,
|
|
206
|
+
ease: motionEasing.smooth,
|
|
207
|
+
}), children: _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false }) })] }));
|
|
156
208
|
}
|
|
157
209
|
// Render content interleaved with tool calls
|
|
158
210
|
// Group consecutive tool calls with the same batchId or same title
|
|
@@ -164,18 +216,18 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
164
216
|
const flushBatch = () => {
|
|
165
217
|
if (currentBatch.length > 1) {
|
|
166
218
|
// Group multiple consecutive calls (by batchId or same title)
|
|
167
|
-
elements.push(_jsx(
|
|
219
|
+
elements.push(_jsx(ToolOperation, { toolCalls: currentBatch, isGrouped: true }, `group-${currentBatchId || currentBatchTitle}-${currentBatch[0].id}`));
|
|
168
220
|
}
|
|
169
221
|
else if (currentBatch.length === 1) {
|
|
170
|
-
elements.push(_jsx("div", { children: _jsx(
|
|
222
|
+
elements.push(_jsx("div", { children: _jsx(ToolOperation, { toolCalls: [currentBatch[0]], isGrouped: false }) }, `tool-${currentBatch[0].id}`));
|
|
171
223
|
}
|
|
172
224
|
currentBatch = [];
|
|
173
225
|
currentBatchId = undefined;
|
|
174
226
|
currentBatchTitle = undefined;
|
|
175
227
|
};
|
|
176
228
|
// Separate preliminary tool calls - they should render at the end, not break text
|
|
177
|
-
const preliminaryToolCalls = sortedToolCalls.filter(
|
|
178
|
-
const nonPreliminaryToolCalls = sortedToolCalls.filter((tc) => !
|
|
229
|
+
const preliminaryToolCalls = sortedToolCalls.filter(isPreliminaryToolCall);
|
|
230
|
+
const nonPreliminaryToolCalls = sortedToolCalls.filter((tc) => !isPreliminaryToolCall(tc));
|
|
179
231
|
// Process non-preliminary tool calls inline with text
|
|
180
232
|
nonPreliminaryToolCalls.forEach((toolCall, index) => {
|
|
181
233
|
const position = toolCall.contentPosition ?? message.content.length;
|
|
@@ -185,7 +237,22 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
185
237
|
flushBatch();
|
|
186
238
|
const textChunk = message.content.slice(currentPosition, position);
|
|
187
239
|
if (textChunk) {
|
|
188
|
-
elements.push(_jsx(
|
|
240
|
+
elements.push(_jsx(motion.div, { initial: {
|
|
241
|
+
filter: "blur(12px)",
|
|
242
|
+
opacity: 0,
|
|
243
|
+
y: 12,
|
|
244
|
+
}, animate: {
|
|
245
|
+
filter: "blur(0px)",
|
|
246
|
+
opacity: 1,
|
|
247
|
+
y: 0,
|
|
248
|
+
}, exit: {
|
|
249
|
+
filter: "blur(12px)",
|
|
250
|
+
opacity: 0,
|
|
251
|
+
y: -12,
|
|
252
|
+
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
253
|
+
duration: 0.4,
|
|
254
|
+
ease: motionEasing.smooth,
|
|
255
|
+
}), children: _jsx(Response, { content: textChunk, isStreaming: false, showEmpty: false }) }, `text-before-${toolCall.id}`));
|
|
189
256
|
}
|
|
190
257
|
}
|
|
191
258
|
// Check if this tool call should be batched (by batchId or consecutive same title)
|
|
@@ -223,20 +290,65 @@ export const MessageContent = React.forwardRef(({ role: roleProp, variant, isStr
|
|
|
223
290
|
if (currentPosition < message.content.length) {
|
|
224
291
|
const remainingText = message.content.slice(currentPosition);
|
|
225
292
|
if (remainingText) {
|
|
226
|
-
elements.push(_jsx(
|
|
293
|
+
elements.push(_jsx(motion.div, { initial: {
|
|
294
|
+
filter: "blur(12px)",
|
|
295
|
+
opacity: 0,
|
|
296
|
+
y: 12,
|
|
297
|
+
}, animate: {
|
|
298
|
+
filter: "blur(0px)",
|
|
299
|
+
opacity: 1,
|
|
300
|
+
y: 0,
|
|
301
|
+
}, exit: {
|
|
302
|
+
filter: "blur(12px)",
|
|
303
|
+
opacity: 0,
|
|
304
|
+
y: -12,
|
|
305
|
+
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
306
|
+
duration: 0.4,
|
|
307
|
+
ease: motionEasing.smooth,
|
|
308
|
+
}), children: _jsx(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }) }, "text-end"));
|
|
227
309
|
}
|
|
228
310
|
}
|
|
229
|
-
// Render preliminary (
|
|
311
|
+
// Render preliminary (selecting) tool calls at the end, grouped
|
|
230
312
|
if (preliminaryToolCalls.length > 0) {
|
|
231
313
|
if (preliminaryToolCalls.length > 1) {
|
|
232
|
-
elements.push(_jsx(
|
|
314
|
+
elements.push(_jsx(ToolOperation, { toolCalls: preliminaryToolCalls, isGrouped: true }, `selecting-group-${preliminaryToolCalls[0].id}`));
|
|
233
315
|
}
|
|
234
316
|
else {
|
|
235
|
-
elements.push(_jsx("div", { children: _jsx(
|
|
317
|
+
elements.push(_jsx("div", { children: _jsx(ToolOperation, { toolCalls: [preliminaryToolCalls[0]], isGrouped: false }) }, `tool-${preliminaryToolCalls[0].id}`));
|
|
236
318
|
}
|
|
237
319
|
}
|
|
238
|
-
return _jsx(
|
|
239
|
-
})()) : (_jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx(
|
|
320
|
+
return (_jsx(AnimatePresence, { mode: "popLayout", children: elements }));
|
|
321
|
+
})()) : (_jsxs("div", { className: "flex flex-col gap-2", children: [message.images && message.images.length > 0 && (_jsx(motion.div, { className: "flex flex-wrap gap-2", initial: {
|
|
322
|
+
filter: "blur(12px)",
|
|
323
|
+
opacity: 0,
|
|
324
|
+
y: 12,
|
|
325
|
+
}, animate: {
|
|
326
|
+
filter: "blur(0px)",
|
|
327
|
+
opacity: 1,
|
|
328
|
+
y: 0,
|
|
329
|
+
}, exit: {
|
|
330
|
+
filter: "blur(12px)",
|
|
331
|
+
opacity: 0,
|
|
332
|
+
y: -12,
|
|
333
|
+
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
334
|
+
duration: 0.5,
|
|
335
|
+
ease: motionEasing.smooth,
|
|
336
|
+
}), 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(motion.div, { className: "whitespace-pre-wrap", initial: {
|
|
337
|
+
filter: "blur(12px)",
|
|
338
|
+
opacity: 0,
|
|
339
|
+
y: 12,
|
|
340
|
+
}, animate: {
|
|
341
|
+
filter: "blur(0px)",
|
|
342
|
+
opacity: 1,
|
|
343
|
+
y: 0,
|
|
344
|
+
}, exit: {
|
|
345
|
+
filter: "blur(12px)",
|
|
346
|
+
opacity: 0,
|
|
347
|
+
y: -12,
|
|
348
|
+
}, transition: getTransition(shouldReduceMotion ?? false, {
|
|
349
|
+
duration: 0.4,
|
|
350
|
+
ease: motionEasing.smooth,
|
|
351
|
+
}), children: message.content }))] }))] }));
|
|
240
352
|
}
|
|
241
353
|
return (_jsx("div", { ref: ref, className: cn(messageContentVariants({ role, variant }), isStreaming && "animate-pulse-subtle", className), ...props, children: content }));
|
|
242
354
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AcpClient } from "../../sdk/client/index.js";
|
|
2
|
+
export interface SessionHistoryProps {
|
|
3
|
+
client: AcpClient | null;
|
|
4
|
+
currentSessionId: string | null;
|
|
5
|
+
onSessionSelect?: ((sessionId: string) => void) | undefined;
|
|
6
|
+
onRenameSession?: ((sessionId: string) => void) | undefined;
|
|
7
|
+
onArchiveSession?: ((sessionId: string) => void) | undefined;
|
|
8
|
+
onDeleteSession?: ((sessionId: string) => void) | undefined;
|
|
9
|
+
}
|
|
10
|
+
export declare function SessionHistory({ client, currentSessionId, onSessionSelect, onRenameSession, onArchiveSession, onDeleteSession, }: SessionHistoryProps): import("react/jsx-runtime").JSX.Element;
|