@townco/ui 0.1.47 → 0.1.49

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 (172) hide show
  1. package/dist/core/hooks/index.d.ts +1 -0
  2. package/dist/core/hooks/index.js +1 -0
  3. package/dist/core/hooks/use-chat-input.d.ts +2 -0
  4. package/dist/core/hooks/use-chat-input.js +11 -1
  5. package/dist/core/hooks/use-chat-messages.d.ts +22 -1
  6. package/dist/core/hooks/use-chat-messages.js +19 -4
  7. package/dist/core/hooks/use-chat-session.js +22 -0
  8. package/dist/core/hooks/use-message-history.d.ts +12 -0
  9. package/dist/core/hooks/use-message-history.js +113 -0
  10. package/dist/core/hooks/use-tool-calls.d.ts +11 -0
  11. package/dist/core/schemas/chat.d.ts +40 -0
  12. package/dist/core/schemas/chat.js +9 -0
  13. package/dist/core/schemas/tool-call.d.ts +34 -0
  14. package/dist/core/schemas/tool-call.js +27 -0
  15. package/dist/core/store/chat-store.d.ts +7 -0
  16. package/dist/core/store/chat-store.js +46 -0
  17. package/dist/gui/components/ChatEmptyState.d.ts +4 -0
  18. package/dist/gui/components/ChatEmptyState.js +2 -2
  19. package/dist/gui/components/ChatInput.d.ts +21 -1
  20. package/dist/gui/components/ChatInput.js +184 -7
  21. package/dist/gui/components/ChatLayout.d.ts +3 -2
  22. package/dist/gui/components/ChatLayout.js +7 -7
  23. package/dist/gui/components/ChatPanelTabContent.d.ts +17 -0
  24. package/dist/gui/components/ChatPanelTabContent.js +6 -5
  25. package/dist/gui/components/ChatView.d.ts +3 -1
  26. package/dist/gui/components/ChatView.js +81 -49
  27. package/dist/gui/components/ContextUsageButton.d.ts +2 -0
  28. package/dist/gui/components/ContextUsageButton.js +3 -1
  29. package/dist/gui/components/InvokingGroup.d.ts +9 -0
  30. package/dist/gui/components/InvokingGroup.js +16 -0
  31. package/dist/gui/components/MessageContent.js +122 -6
  32. package/dist/gui/components/PanelTabsHeader.d.ts +1 -1
  33. package/dist/gui/components/PanelTabsHeader.js +6 -1
  34. package/dist/gui/components/Response.js +2 -0
  35. package/dist/gui/components/Task.js +3 -3
  36. package/dist/gui/components/TodoListItem.js +1 -1
  37. package/dist/gui/components/ToolCall.js +57 -5
  38. package/dist/gui/components/ToolCallGroup.d.ts +8 -0
  39. package/dist/gui/components/ToolCallGroup.js +29 -0
  40. package/dist/gui/components/index.d.ts +1 -2
  41. package/dist/gui/components/index.js +1 -2
  42. package/dist/sdk/client/acp-client.d.ts +24 -1
  43. package/dist/sdk/client/acp-client.js +28 -7
  44. package/dist/sdk/schemas/message.d.ts +41 -9
  45. package/dist/sdk/schemas/message.js +15 -3
  46. package/dist/sdk/schemas/session.d.ts +75 -10
  47. package/dist/sdk/transports/http.d.ts +14 -0
  48. package/dist/sdk/transports/http.js +130 -36
  49. package/dist/sdk/transports/stdio.d.ts +14 -0
  50. package/dist/sdk/transports/stdio.js +27 -10
  51. package/dist/sdk/transports/types.d.ts +29 -0
  52. package/package.json +3 -3
  53. package/dist/core/hooks/index.d.ts.map +0 -1
  54. package/dist/core/hooks/index.js.map +0 -1
  55. package/dist/core/hooks/use-chat-input.d.ts.map +0 -1
  56. package/dist/core/hooks/use-chat-input.js.map +0 -1
  57. package/dist/core/hooks/use-chat-messages.d.ts.map +0 -1
  58. package/dist/core/hooks/use-chat-messages.js.map +0 -1
  59. package/dist/core/hooks/use-chat-session.d.ts.map +0 -1
  60. package/dist/core/hooks/use-chat-session.js.map +0 -1
  61. package/dist/core/index.d.ts.map +0 -1
  62. package/dist/core/index.js.map +0 -1
  63. package/dist/core/lib/logger.d.ts +0 -24
  64. package/dist/core/lib/logger.js +0 -108
  65. package/dist/core/schemas/chat.d.ts.map +0 -1
  66. package/dist/core/schemas/chat.js.map +0 -1
  67. package/dist/core/schemas/index.d.ts.map +0 -1
  68. package/dist/core/schemas/index.js.map +0 -1
  69. package/dist/core/store/chat-store.d.ts.map +0 -1
  70. package/dist/core/store/chat-store.js.map +0 -1
  71. package/dist/gui/components/Button.d.ts.map +0 -1
  72. package/dist/gui/components/Button.js.map +0 -1
  73. package/dist/gui/components/Card.d.ts.map +0 -1
  74. package/dist/gui/components/Card.js.map +0 -1
  75. package/dist/gui/components/ChatInput.d.ts.map +0 -1
  76. package/dist/gui/components/ChatInput.js.map +0 -1
  77. package/dist/gui/components/ChatSecondaryPanel.d.ts.map +0 -1
  78. package/dist/gui/components/ChatSecondaryPanel.js.map +0 -1
  79. package/dist/gui/components/ChatStatus.d.ts.map +0 -1
  80. package/dist/gui/components/ChatStatus.js.map +0 -1
  81. package/dist/gui/components/Conversation.d.ts.map +0 -1
  82. package/dist/gui/components/Conversation.js.map +0 -1
  83. package/dist/gui/components/Dialog.d.ts.map +0 -1
  84. package/dist/gui/components/Dialog.js.map +0 -1
  85. package/dist/gui/components/HeightTransition.d.ts.map +0 -1
  86. package/dist/gui/components/HeightTransition.js.map +0 -1
  87. package/dist/gui/components/Input.d.ts.map +0 -1
  88. package/dist/gui/components/Input.js.map +0 -1
  89. package/dist/gui/components/Label.d.ts.map +0 -1
  90. package/dist/gui/components/Label.js.map +0 -1
  91. package/dist/gui/components/MarkdownRenderer.d.ts.map +0 -1
  92. package/dist/gui/components/MarkdownRenderer.js.map +0 -1
  93. package/dist/gui/components/Message.d.ts.map +0 -1
  94. package/dist/gui/components/Message.js.map +0 -1
  95. package/dist/gui/components/MessageContent.d.ts.map +0 -1
  96. package/dist/gui/components/MessageContent.js.map +0 -1
  97. package/dist/gui/components/MessageList.d.ts.map +0 -1
  98. package/dist/gui/components/MessageList.js.map +0 -1
  99. package/dist/gui/components/Reasoning.d.ts.map +0 -1
  100. package/dist/gui/components/Reasoning.js.map +0 -1
  101. package/dist/gui/components/Response.d.ts.map +0 -1
  102. package/dist/gui/components/Response.js.map +0 -1
  103. package/dist/gui/components/Select.d.ts.map +0 -1
  104. package/dist/gui/components/Select.js.map +0 -1
  105. package/dist/gui/components/Tabs.d.ts.map +0 -1
  106. package/dist/gui/components/Tabs.js.map +0 -1
  107. package/dist/gui/components/Task.d.ts.map +0 -1
  108. package/dist/gui/components/Task.js.map +0 -1
  109. package/dist/gui/components/Textarea.d.ts.map +0 -1
  110. package/dist/gui/components/Textarea.js.map +0 -1
  111. package/dist/gui/components/ThinkingBlock.d.ts.map +0 -1
  112. package/dist/gui/components/ThinkingBlock.js.map +0 -1
  113. package/dist/gui/components/TodoList.d.ts.map +0 -1
  114. package/dist/gui/components/TodoList.js.map +0 -1
  115. package/dist/gui/components/TodoListItem.d.ts.map +0 -1
  116. package/dist/gui/components/TodoListItem.js.map +0 -1
  117. package/dist/gui/components/index.d.ts.map +0 -1
  118. package/dist/gui/components/index.js.map +0 -1
  119. package/dist/gui/index.d.ts.map +0 -1
  120. package/dist/gui/index.js.map +0 -1
  121. package/dist/gui/lib/utils.d.ts.map +0 -1
  122. package/dist/gui/lib/utils.js.map +0 -1
  123. package/dist/index.d.ts.map +0 -1
  124. package/dist/index.js.map +0 -1
  125. package/dist/sdk/client/acp-client.d.ts.map +0 -1
  126. package/dist/sdk/client/acp-client.js.map +0 -1
  127. package/dist/sdk/client/index.d.ts.map +0 -1
  128. package/dist/sdk/client/index.js.map +0 -1
  129. package/dist/sdk/index.d.ts.map +0 -1
  130. package/dist/sdk/index.js.map +0 -1
  131. package/dist/sdk/schemas/agent.d.ts.map +0 -1
  132. package/dist/sdk/schemas/agent.js.map +0 -1
  133. package/dist/sdk/schemas/index.d.ts.map +0 -1
  134. package/dist/sdk/schemas/index.js.map +0 -1
  135. package/dist/sdk/schemas/message.d.ts.map +0 -1
  136. package/dist/sdk/schemas/message.js.map +0 -1
  137. package/dist/sdk/schemas/session.d.ts.map +0 -1
  138. package/dist/sdk/schemas/session.js.map +0 -1
  139. package/dist/sdk/transports/http.d.ts.map +0 -1
  140. package/dist/sdk/transports/http.js.map +0 -1
  141. package/dist/sdk/transports/index.d.ts.map +0 -1
  142. package/dist/sdk/transports/index.js.map +0 -1
  143. package/dist/sdk/transports/stdio.d.ts.map +0 -1
  144. package/dist/sdk/transports/stdio.js.map +0 -1
  145. package/dist/sdk/transports/types.d.ts.map +0 -1
  146. package/dist/sdk/transports/types.js.map +0 -1
  147. package/dist/sdk/transports/websocket.d.ts.map +0 -1
  148. package/dist/sdk/transports/websocket.js.map +0 -1
  149. package/dist/test-http-client.d.ts +0 -11
  150. package/dist/test-http-client.d.ts.map +0 -1
  151. package/dist/test-http-client.js +0 -147
  152. package/dist/test-http-client.js.map +0 -1
  153. package/dist/test-http-transport.d.ts +0 -11
  154. package/dist/test-http-transport.d.ts.map +0 -1
  155. package/dist/test-http-transport.js +0 -127
  156. package/dist/test-http-transport.js.map +0 -1
  157. package/dist/tui/components/ChatView.d.ts.map +0 -1
  158. package/dist/tui/components/ChatView.js.map +0 -1
  159. package/dist/tui/components/GameOfLife.d.ts.map +0 -1
  160. package/dist/tui/components/GameOfLife.js.map +0 -1
  161. package/dist/tui/components/InputBox.d.ts.map +0 -1
  162. package/dist/tui/components/InputBox.js.map +0 -1
  163. package/dist/tui/components/MessageList.d.ts.map +0 -1
  164. package/dist/tui/components/MessageList.js.map +0 -1
  165. package/dist/tui/components/ReadlineInput.d.ts.map +0 -1
  166. package/dist/tui/components/ReadlineInput.js.map +0 -1
  167. package/dist/tui/components/StatusBar.d.ts.map +0 -1
  168. package/dist/tui/components/StatusBar.js.map +0 -1
  169. package/dist/tui/components/index.d.ts.map +0 -1
  170. package/dist/tui/components/index.js.map +0 -1
  171. package/dist/tui/index.d.ts.map +0 -1
  172. package/dist/tui/index.js.map +0 -1
@@ -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
- setPanelSize("small");
16
- setActiveTab("files");
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(_Fragment, { children: children({ openFiles }) });
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
- 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" }), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle sidebar", onClick: () => {
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 with your tasks. Start a conversation by typing a message below.");
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
- // TODO: Replace with useChatStore((state) => state.todos) when todos are added to the store
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 }) })) })) : (_jsx("div", { className: "flex flex-col px-4", children: messages.map((message, index) => {
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: _jsxs(ChatInputRoot, { client: client, startSession: startSession, children: [_jsx(ChatInputCommandMenu, { commands: commandMenuItems }), _jsx(ChatInputField, { placeholder: placeholder, autoFocus: true }), _jsxs(ChatInputToolbar, { children: [_jsxs("div", { className: "flex items-baseline gap-1", children: [_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, {}), 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" }) })] })] })] }) })] })] }), isLargeScreen && (_jsx(ChatLayout.Aside, { breakpoint: "lg", children: _jsx(AsideTabs, {}) }))] }));
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
  }
@@ -1,6 +1,8 @@
1
1
  import * as React from "react";
2
2
  export interface ContextSize {
3
3
  systemPromptTokens: number;
4
+ toolOverheadTokens?: number;
5
+ mcpOverheadTokens?: number;
4
6
  userMessagesTokens: number;
5
7
  assistantMessagesTokens: number;
6
8
  toolInputTokens: number;
@@ -32,6 +32,8 @@ export const ContextUsageButton = React.forwardRef(({ contextSize, modelContextW
32
32
  const center = size / 2;
33
33
  const circumference = 2 * Math.PI * radius;
34
34
  const offset = circumference - (clampedPercentage / 100) * circumference;
35
- return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { ref: ref, type: "button", variant: "ghost", size: "icon", className: cn("rounded-full cursor-default", colorClass, className), ...props, children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "transform -rotate-90", children: [_jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", className: "opacity-20" }), _jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round", className: "transition-all duration-300 ease-in-out" })] }) }) }), _jsx(TooltipContent, { side: "top", align: "center", className: "p-3", children: _jsxs("div", { className: "space-y-2 font-mono text-xs", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "System Prompt:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.systemPromptTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.systemPromptTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "User Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.userMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.userMessagesTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Assistant Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.assistantMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.assistantMessagesTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Tool Inputs:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.toolInputTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolInputTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Tool Results:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.toolResultsTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolResultsTokens), ")"] })] })] })] }), _jsx("div", { className: "border-t border-border pt-2", children: _jsxs("div", { className: cn("flex justify-end gap-2 font-semibold", colorClass), children: [_jsx("span", { children: actualTokens.toLocaleString() }), _jsxs("span", { children: ["(", formattedPercentage, ")"] })] }) })] }) })] }) }));
35
+ return (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(Button, { ref: ref, type: "button", variant: "ghost", size: "icon", className: cn("rounded-full cursor-default", colorClass, className), ...props, children: _jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "transform -rotate-90", children: [_jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", className: "opacity-20" }), _jsx("circle", { cx: center, cy: center, r: radius, stroke: "currentColor", strokeWidth: strokeWidth, fill: "transparent", strokeDasharray: circumference, strokeDashoffset: offset, strokeLinecap: "round", className: "transition-all duration-300 ease-in-out" })] }) }) }), _jsx(TooltipContent, { side: "top", align: "center", className: "p-3", children: _jsxs("div", { className: "space-y-2 font-mono text-xs", children: [_jsxs("div", { className: "space-y-1", children: [_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "System Prompt:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.systemPromptTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.systemPromptTokens), ")"] })] })] }), contextSize.toolOverheadTokens !== undefined &&
36
+ contextSize.toolOverheadTokens > 0 && (_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Tools Definition:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.toolOverheadTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolOverheadTokens), ")"] })] })] })), contextSize.mcpOverheadTokens !== undefined &&
37
+ contextSize.mcpOverheadTokens > 0 && (_jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "MCPs Definition:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.mcpOverheadTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.mcpOverheadTokens), ")"] })] })] })), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "User Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.userMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.userMessagesTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Assistant Messages:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.assistantMessagesTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.assistantMessagesTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Tool Inputs:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.toolInputTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolInputTokens), ")"] })] })] }), _jsxs("div", { className: "flex justify-between gap-6", children: [_jsx("span", { className: "text-muted-foreground", children: "Tool Results:" }), _jsxs("div", { className: "flex gap-1 items-baseline", children: [_jsx("span", { children: contextSize.toolResultsTokens.toLocaleString() }), _jsxs("span", { className: "text-muted-foreground text-[10px]", children: ["(", calculatePercentage(contextSize.toolResultsTokens), ")"] })] })] })] }), _jsx("div", { className: "border-t border-border pt-2", children: _jsxs("div", { className: cn("flex justify-end gap-2 font-semibold", colorClass), children: [_jsx("span", { children: actualTokens.toLocaleString() }), _jsxs("span", { children: ["(", formattedPercentage, ")"] })] }) })] }) })] }) }));
36
38
  });
37
39
  ContextUsageButton.displayName = "ContextUsageButton";
@@ -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
- return (_jsxs(_Fragment, { children: [sortedToolCalls.length > 0 && (_jsx("div", { className: "flex flex-col gap-2 mb-1", children: sortedToolCalls.map((toolCall) => (_jsx(ToolCall, { toolCall: toolCall }, toolCall.id))) })), _jsx(Response, { content: message.content, isStreaming: message.isStreaming, showEmpty: false })] }));
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
- sortedToolCalls.forEach((toolCall) => {
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
- // Add the tool call
74
- elements.push(_jsx("div", { children: _jsx(ToolCall, { toolCall: toolCall }) }, `tool-${toolCall.id}`));
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 = "Task";
34
- export const TaskList = React.forwardRef(({ tasks, collapsible = true, onTaskClick, emptyMessage = "No tasks yet.", className, ...props }, ref) => {
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 = "TaskList";
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: isCompleted ? (_jsx(CircleCheck, { className: "size-4 text-muted-foreground" })) : (_jsx(Circle, { className: "size-4 text-foreground" })) }), _jsx("p", { className: cn("flex-1 text-foreground",
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