@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.
Files changed (56) hide show
  1. package/dist/core/hooks/use-chat-messages.d.ts +6 -1
  2. package/dist/core/hooks/use-chat-session.d.ts +1 -1
  3. package/dist/core/hooks/use-tool-calls.d.ts +6 -1
  4. package/dist/core/schemas/chat.d.ts +10 -0
  5. package/dist/core/schemas/tool-call.d.ts +5 -0
  6. package/dist/core/schemas/tool-call.js +8 -0
  7. package/dist/core/utils/tool-call-state.d.ts +30 -0
  8. package/dist/core/utils/tool-call-state.js +73 -0
  9. package/dist/core/utils/tool-summary.d.ts +13 -0
  10. package/dist/core/utils/tool-summary.js +172 -0
  11. package/dist/core/utils/tool-verbiage.d.ts +28 -0
  12. package/dist/core/utils/tool-verbiage.js +185 -0
  13. package/dist/gui/components/AppSidebar.d.ts +22 -0
  14. package/dist/gui/components/AppSidebar.js +22 -0
  15. package/dist/gui/components/Button.d.ts +1 -1
  16. package/dist/gui/components/ChatLayout.d.ts +5 -0
  17. package/dist/gui/components/ChatLayout.js +239 -132
  18. package/dist/gui/components/ChatView.js +42 -118
  19. package/dist/gui/components/MessageContent.js +151 -39
  20. package/dist/gui/components/SessionHistory.d.ts +10 -0
  21. package/dist/gui/components/SessionHistory.js +101 -0
  22. package/dist/gui/components/SessionHistoryItem.d.ts +11 -0
  23. package/dist/gui/components/SessionHistoryItem.js +24 -0
  24. package/dist/gui/components/Sheet.d.ts +25 -0
  25. package/dist/gui/components/Sheet.js +36 -0
  26. package/dist/gui/components/Sidebar.d.ts +65 -0
  27. package/dist/gui/components/Sidebar.js +231 -0
  28. package/dist/gui/components/SidebarToggle.d.ts +3 -0
  29. package/dist/gui/components/SidebarToggle.js +9 -0
  30. package/dist/gui/components/ToolCallList.js +3 -3
  31. package/dist/gui/components/ToolOperation.d.ts +11 -0
  32. package/dist/gui/components/ToolOperation.js +289 -0
  33. package/dist/gui/components/WorkProgress.d.ts +20 -0
  34. package/dist/gui/components/WorkProgress.js +79 -0
  35. package/dist/gui/components/index.d.ts +8 -1
  36. package/dist/gui/components/index.js +9 -1
  37. package/dist/gui/hooks/index.d.ts +1 -0
  38. package/dist/gui/hooks/index.js +1 -0
  39. package/dist/gui/hooks/use-mobile.d.ts +1 -0
  40. package/dist/gui/hooks/use-mobile.js +15 -0
  41. package/dist/gui/index.d.ts +1 -0
  42. package/dist/gui/index.js +2 -0
  43. package/dist/gui/lib/motion.d.ts +55 -0
  44. package/dist/gui/lib/motion.js +217 -0
  45. package/dist/sdk/schemas/session.d.ts +11 -6
  46. package/dist/sdk/transports/types.d.ts +5 -0
  47. package/package.json +8 -7
  48. package/src/styles/global.css +128 -1
  49. package/dist/gui/components/InvokingGroup.d.ts +0 -9
  50. package/dist/gui/components/InvokingGroup.js +0 -16
  51. package/dist/gui/components/SubagentStream.d.ts +0 -23
  52. package/dist/gui/components/SubagentStream.js +0 -98
  53. package/dist/gui/components/ToolCall.d.ts +0 -8
  54. package/dist/gui/components/ToolCall.js +0 -234
  55. package/dist/gui/components/ToolCallGroup.d.ts +0 -8
  56. 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, ChevronDown, ChevronUp, Code, PanelRight, Plus, Settings, Sparkles, X, } from "lucide-react";
4
- import { useCallback, useEffect, useState } from "react";
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, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, FilesTabContent, IconButton, Message, MessageContent, PanelTabsHeader, SettingsTabContent, SourcesTabContent, Tabs, TabsContent, ThemeToggle, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./index.js";
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
- // 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
- // 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, client, currentSessionId, }) {
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: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx(SessionSwitcher, { agentName: agentName, client: client, currentSessionId: currentSessionId }) })), !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") }) })] }));
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, client, }) {
137
- const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
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: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx(SessionSwitcher, { agentName: agentName, client: client, currentSessionId: sessionId }) })), !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: sessionId ? "View session in debugger" : "Open debugger" }) })] }) })), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle sidebar", onClick: () => {
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: [_jsx(SidebarHotkey, {}), _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, client: client, ...(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, titleElement: _jsx(SessionSwitcher, { agentName: agentName, client: client, currentSessionId: sessionId }), description: agentDescription, suggestedPrompts: suggestedPrompts, onPromptClick: (prompt) => {
295
- sendMessage(prompt);
296
- setPlaceholder("Type a message or / for commands...");
297
- logger.info("Prompt clicked", { prompt });
298
- }, onPromptHover: handlePromptHover, onPromptLeave: handlePromptLeave, onOpenFiles: openFiles, onOpenSettings: openSettings, toolsAndMcpsCount: agentTools.length +
299
- agentMcps.length +
300
- agentSubagents.length }) })) })) : (_jsx("div", { className: "flex flex-col px-4", children: messages.map((message, index) => {
301
- // Calculate dynamic spacing based on message sequence
302
- const isFirst = index === 0;
303
- const previousMessage = isFirst ? null : messages[index - 1];
304
- let spacingClass = "mt-2";
305
- if (isFirst) {
306
- // First message needs more top margin if it's an assistant initial message
307
- spacingClass =
308
- message.role === "assistant" ? "mt-8" : "mt-2";
309
- }
310
- else if (message.role === "user") {
311
- // User message usually starts a new turn
312
- spacingClass =
313
- previousMessage?.role === "user" ? "mt-4" : "mt-4";
314
- }
315
- else if (message.role === "assistant") {
316
- // Assistant message is usually a response
317
- spacingClass =
318
- previousMessage?.role === "assistant" ? "mt-2" : "mt-6";
319
- }
320
- return (_jsx(Message, { message: message, className: spacingClass, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id));
321
- }) })) }), _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 }) }))] }));
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 { ToolCall } from "./ToolCall.js";
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(Reasoning, { content: thinking, isStreaming: message.isStreaming, mode: thinkingDisplayStyle, autoCollapse: true })), isWaiting && streamingStartTime && (_jsx("div", { className: "flex items-center gap-2", children: _jsx("div", { className: "size-2.5 rounded-full bg-foreground animate-pulse-scale" }) })), message.role === "assistant" ? ((() => {
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 currentInvokingGroup = [];
102
+ let currentSelectingGroup = [];
66
103
  let currentConsecutiveGroup = [];
67
104
  let currentConsecutiveTitle = null;
68
- const flushInvokingGroup = () => {
69
- if (currentInvokingGroup.length > 1) {
105
+ const flushSelectingGroup = () => {
106
+ if (currentSelectingGroup.length > 1) {
70
107
  result.push({
71
- type: "invoking",
72
- toolCalls: currentInvokingGroup,
108
+ type: "selecting",
109
+ toolCalls: currentSelectingGroup,
73
110
  });
74
111
  }
75
- else if (currentInvokingGroup.length === 1) {
76
- result.push(currentInvokingGroup[0]);
112
+ else if (currentSelectingGroup.length === 1) {
113
+ result.push(currentSelectingGroup[0]);
77
114
  }
78
- currentInvokingGroup = [];
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
- flushInvokingGroup();
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 (invoking) tool calls
110
- else if (isPreliminary(tc)) {
146
+ // Handle consecutive preliminary (selecting) tool calls
147
+ else if (isPreliminaryToolCall(tc)) {
111
148
  flushConsecutiveGroup();
112
- currentInvokingGroup.push(tc);
149
+ currentSelectingGroup.push(tc);
113
150
  }
114
151
  // Regular tool call - group consecutive same-title calls (e.g., subagent)
115
152
  else {
116
- flushInvokingGroup();
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
- flushInvokingGroup();
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(ToolCallGroup, { toolCalls: item.toolCalls }, `batch-${item.toolCalls[0]?.batchId || index}`));
177
+ return (_jsx(ToolOperation, { toolCalls: item.toolCalls, isGrouped: true }, `batch-${item.toolCalls[0]?.batchId || index}`));
141
178
  }
142
- // Invoking group (consecutive preliminary tool calls)
179
+ // Selecting group (consecutive preliminary tool calls)
143
180
  if (typeof item === "object" &&
144
181
  "type" in item &&
145
- item.type === "invoking") {
146
- return (_jsx(InvokingGroup, { toolCalls: item.toolCalls }, `invoking-${item.toolCalls[0]?.id || index}`));
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(ToolCall, { toolCall: item }, item.id));
186
+ return (_jsx(ToolOperation, { toolCalls: [item], isGrouped: false }, item.id));
150
187
  };
151
- // If no tool calls or they don't have positions, render old way
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(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false })] }));
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(ToolCallGroup, { toolCalls: currentBatch }, `group-${currentBatchId || currentBatchTitle}-${currentBatch[0].id}`));
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(ToolCall, { toolCall: currentBatch[0] }) }, `tool-${currentBatch[0].id}`));
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(isPreliminary);
178
- const nonPreliminaryToolCalls = sortedToolCalls.filter((tc) => !isPreliminary(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(Response, { content: textChunk, isStreaming: false, showEmpty: false }, `text-before-${toolCall.id}`));
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(Response, { content: remainingText, isStreaming: message.isStreaming, showEmpty: false }, "text-end"));
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 (invoking) tool calls at the end, grouped
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(InvokingGroup, { toolCalls: preliminaryToolCalls }, `invoking-group-${preliminaryToolCalls[0].id}`));
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(ToolCall, { toolCall: preliminaryToolCalls[0] }) }, `tool-${preliminaryToolCalls[0].id}`));
317
+ elements.push(_jsx("div", { children: _jsx(ToolOperation, { toolCalls: [preliminaryToolCalls[0]], isGrouped: false }) }, `tool-${preliminaryToolCalls[0].id}`));
236
318
  }
237
319
  }
238
- return _jsx(_Fragment, { children: elements });
239
- })()) : (_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 }))] }))] }));
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;