@townco/ui 0.1.50 → 0.1.52

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 (33) 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-messages.d.ts +84 -0
  4. package/dist/core/hooks/use-chat-session.js +20 -3
  5. package/dist/core/hooks/use-subagent-stream.d.ts +28 -0
  6. package/dist/core/hooks/use-subagent-stream.js +254 -0
  7. package/dist/core/hooks/use-tool-calls.d.ts +84 -0
  8. package/dist/core/schemas/chat.d.ts +188 -0
  9. package/dist/core/schemas/tool-call.d.ts +286 -0
  10. package/dist/core/schemas/tool-call.js +53 -0
  11. package/dist/gui/components/ChatEmptyState.d.ts +2 -0
  12. package/dist/gui/components/ChatEmptyState.js +2 -2
  13. package/dist/gui/components/ChatLayout.d.ts +2 -0
  14. package/dist/gui/components/ChatLayout.js +70 -1
  15. package/dist/gui/components/ChatPanelTabContent.js +2 -2
  16. package/dist/gui/components/ChatSecondaryPanel.js +1 -1
  17. package/dist/gui/components/ChatView.js +85 -12
  18. package/dist/gui/components/PanelTabsHeader.js +1 -1
  19. package/dist/gui/components/SubAgentDetails.d.ts +27 -0
  20. package/dist/gui/components/SubAgentDetails.js +121 -0
  21. package/dist/gui/components/TodoList.js +12 -2
  22. package/dist/gui/components/ToolCall.js +41 -8
  23. package/dist/gui/components/index.d.ts +1 -0
  24. package/dist/gui/components/index.js +1 -0
  25. package/dist/sdk/client/acp-client.d.ts +9 -1
  26. package/dist/sdk/client/acp-client.js +10 -0
  27. package/dist/sdk/schemas/message.d.ts +2 -2
  28. package/dist/sdk/schemas/session.d.ts +96 -0
  29. package/dist/sdk/transports/http.d.ts +12 -1
  30. package/dist/sdk/transports/http.js +77 -1
  31. package/dist/sdk/transports/stdio.d.ts +3 -0
  32. package/dist/sdk/transports/types.d.ts +34 -0
  33. package/package.json +3 -3
@@ -38,11 +38,12 @@ const ChatLayoutBody = React.forwardRef(({ showToaster = true, className, childr
38
38
  return (_jsxs("div", { ref: ref, className: cn("relative flex flex-1 flex-col overflow-hidden", className), ...props, children: [children, showToaster && _jsx(Toaster, {})] }));
39
39
  });
40
40
  ChatLayoutBody.displayName = "ChatLayout.Body";
41
- const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, ...props }, ref) => {
41
+ const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChange, showScrollToBottom = true, initialScrollToBottom = true, ...props }, ref) => {
42
42
  const [showScrollButton, setShowScrollButton] = React.useState(false);
43
43
  const scrollContainerRef = React.useRef(null);
44
44
  const wasAtBottomRef = React.useRef(true); // Track if user was at bottom before content update
45
45
  const isAutoScrollingRef = React.useRef(false); // Track if we're programmatically scrolling
46
+ const hasInitialScrolledRef = React.useRef(false); // Track if initial scroll has happened
46
47
  // Merge refs
47
48
  React.useImperativeHandle(ref, () => scrollContainerRef.current);
48
49
  // Check if user is at bottom of scroll
@@ -102,6 +103,74 @@ const ChatLayoutMessages = React.forwardRef(({ className, children, onScrollChan
102
103
  checkScrollPosition();
103
104
  }
104
105
  }, [children, scrollToBottom, checkScrollPosition]);
106
+ // Track last scroll height to detect when content stops loading
107
+ const lastScrollHeightRef = React.useRef(0);
108
+ const scrollStableCountRef = React.useRef(0);
109
+ // Scroll to bottom on initial mount and during session loading
110
+ // Keep scrolling until content stabilizes (no more changes)
111
+ React.useEffect(() => {
112
+ if (!initialScrollToBottom)
113
+ return;
114
+ const container = scrollContainerRef.current;
115
+ if (!container)
116
+ return;
117
+ const scrollToBottomInstant = () => {
118
+ container.scrollTop = container.scrollHeight;
119
+ wasAtBottomRef.current = true;
120
+ };
121
+ // Check if content has stabilized (scrollHeight hasn't changed)
122
+ const currentHeight = container.scrollHeight;
123
+ if (currentHeight === lastScrollHeightRef.current) {
124
+ scrollStableCountRef.current++;
125
+ }
126
+ else {
127
+ scrollStableCountRef.current = 0;
128
+ lastScrollHeightRef.current = currentHeight;
129
+ }
130
+ // If content is still loading (height changing) or we haven't scrolled yet,
131
+ // keep auto-scrolling. Stop after content is stable for a few renders.
132
+ if (scrollStableCountRef.current < 3) {
133
+ isAutoScrollingRef.current = true;
134
+ scrollToBottomInstant();
135
+ hasInitialScrolledRef.current = true;
136
+ }
137
+ else {
138
+ // Content is stable, stop auto-scrolling
139
+ isAutoScrollingRef.current = false;
140
+ }
141
+ }, [initialScrollToBottom, children]);
142
+ // Also use a timer-based approach as backup for session replay
143
+ // which may not trigger children changes
144
+ React.useEffect(() => {
145
+ if (!initialScrollToBottom)
146
+ return;
147
+ const container = scrollContainerRef.current;
148
+ if (!container)
149
+ return;
150
+ // Keep scrolling to bottom for the first 2 seconds of session load
151
+ // to catch async message replay
152
+ let cancelled = false;
153
+ const scrollInterval = setInterval(() => {
154
+ if (cancelled)
155
+ return;
156
+ if (container.scrollHeight > container.clientHeight) {
157
+ isAutoScrollingRef.current = true;
158
+ container.scrollTop = container.scrollHeight;
159
+ wasAtBottomRef.current = true;
160
+ hasInitialScrolledRef.current = true;
161
+ }
162
+ }, 100);
163
+ // Stop after 2 seconds
164
+ const timeout = setTimeout(() => {
165
+ clearInterval(scrollInterval);
166
+ isAutoScrollingRef.current = false;
167
+ }, 2000);
168
+ return () => {
169
+ cancelled = true;
170
+ clearInterval(scrollInterval);
171
+ clearTimeout(timeout);
172
+ };
173
+ }, [initialScrollToBottom]); // Only run once on mount
105
174
  // Check scroll position on mount
106
175
  React.useEffect(() => {
107
176
  if (!isAutoScrollingRef.current) {
@@ -6,7 +6,7 @@ import { FileSystemView } from "./FileSystemView.js";
6
6
  import { SourceListItem } from "./SourceListItem.js";
7
7
  import { TodoList } from "./TodoList.js";
8
8
  export const TodoTabContent = React.forwardRef(({ todos = [], className, ...props }, ref) => {
9
- return (_jsx("div", { ref: ref, className: cn("space-y-2", className), ...props, children: _jsx(TodoList, { todos: todos }) }));
9
+ return (_jsx("div", { ref: ref, className: cn("h-full", className), ...props, children: _jsx(TodoList, { todos: todos, className: "h-full" }) }));
10
10
  });
11
11
  TodoTabContent.displayName = "TodoTabContent";
12
12
  export const FilesTabContent = React.forwardRef(({ files = [], provider, onFileSelect, className, ...props }, ref) => {
@@ -42,6 +42,6 @@ export const DatabaseTabContent = React.forwardRef(({ data, className, ...props
42
42
  });
43
43
  DatabaseTabContent.displayName = "DatabaseTabContent";
44
44
  export const SettingsTabContent = React.forwardRef(({ tools = [], mcps = [], subagents = [], className, ...props }, ref) => {
45
- return (_jsxs("div", { ref: ref, className: cn("space-y-6", className), ...props, children: [_jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Tools" }), tools.length > 0 ? (_jsx("div", { className: "space-y-2", children: tools.map((tool) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: tool.prettyName || tool.name }), tool.description && (_jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-1", children: tool.description }))] }, tool.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No tools available" }))] }), _jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "MCP Servers" }), mcps.length > 0 ? (_jsx("div", { className: "space-y-2", children: mcps.map((mcp) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: mcp.name }), _jsxs("div", { className: "text-caption text-muted-foreground mt-1", children: ["Transport: ", mcp.transport] })] }, mcp.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No MCP servers connected" }))] }), _jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Subagents" }), subagents.length > 0 ? (_jsx("div", { className: "space-y-2", children: subagents.map((subagent) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: subagent.name }), _jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-2", children: subagent.description })] }, subagent.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No subagents available" }))] })] }));
45
+ return (_jsxs("div", { ref: ref, className: cn("space-y-6", className), ...props, children: [_jsxs("div", { className: "space-y-3 p-2", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Tools" }), tools.length > 0 ? (_jsx("div", { className: "space-y-2", children: tools.map((tool) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: tool.prettyName || tool.name }), tool.description && (_jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-1", children: tool.description }))] }, tool.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No tools available" }))] }), _jsxs("div", { className: "space-y-3 p-2", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "MCP Servers" }), mcps.length > 0 ? (_jsx("div", { className: "space-y-2", children: mcps.map((mcp) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: mcp.name }), _jsxs("div", { className: "text-caption text-muted-foreground mt-1", children: ["Transport: ", mcp.transport] })] }, mcp.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No MCP servers connected" }))] }), _jsxs("div", { className: "space-y-3 p-2", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Subagents" }), subagents.length > 0 ? (_jsx("div", { className: "space-y-2", children: subagents.map((subagent) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: subagent.name }), _jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-2", children: subagent.description })] }, subagent.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No subagents available" }))] })] }));
46
46
  });
47
47
  SettingsTabContent.displayName = "SettingsTabContent";
@@ -60,7 +60,7 @@ export const ChatSecondaryPanel = React.forwardRef(({ client, todos, variant = "
60
60
  // Pills variant - Simple background highlight (Figma design)
61
61
  _jsx(TabsList, { className: cn("w-full justify-start bg-transparent p-0 h-auto", "gap-1"), children: tabs.map((tab) => {
62
62
  const Icon = tab.icon;
63
- return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-paragraph-sm font-medium", "data-[state=active]:bg-zinc-100 data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
63
+ return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-paragraph-sm font-medium", "data-[state=active]:bg-accent data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
64
64
  }) })) : (
65
65
  // Animated variant - Clip-path animation (original style)
66
66
  _jsxs("div", { className: "relative mb-4 border-border", children: [_jsx(TabsList, { className: "bg-transparent p-0 h-auto rounded-none w-full border-none", children: tabs.map((tab) => (_jsx(TabsTrigger, { value: tab.id, className: "px-3 py-1 text-paragraph-sm font-medium rounded-none text-foreground opacity-60 data-[state=active]:opacity-100 data-[state=active]:bg-transparent data-[state=active]:shadow-none", children: tab.label }, tab.id))) }), _jsx("div", { ref: containerRef, className: "absolute top-0 left-0 w-full overflow-hidden z-10 pointer-events-none", style: {
@@ -1,12 +1,12 @@
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, ChevronUp, Code, PanelRight, Settings, Sparkles, X, } from "lucide-react";
4
- import { useEffect, useState } from "react";
3
+ import { ArrowUp, Bug, ChevronDown, ChevronUp, Code, PanelRight, Plus, Settings, Sparkles, X, } from "lucide-react";
4
+ import { useCallback, 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
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, SettingsTabContent, SourcesTabContent, Tabs, TabsContent, ThemeToggle, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./index.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";
10
10
  const logger = createLogger("gui");
11
11
  // Helper component to provide openFiles callback
12
12
  function OpenFilesButton({ children, }) {
@@ -47,6 +47,69 @@ function SidebarHotkey() {
47
47
  }, [panelSize, setPanelSize]);
48
48
  return null;
49
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
+ }
50
113
  // Chat input with attachment handling
51
114
  function ChatInputWithAttachments({ client, startSession, placeholder, latestContextSize, currentModel, commandMenuItems, }) {
52
115
  const attachedFiles = useChatStore((state) => state.input.attachedFiles);
@@ -62,20 +125,24 @@ function ChatInputWithAttachments({ client, startSession, placeholder, latestCon
62
125
  // Controlled Tabs component for the aside panel
63
126
  function AsideTabs({ todos, tools, mcps, subagents, }) {
64
127
  const { activeTab, setActiveTab } = ChatLayout.useChatLayoutContext();
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 ?? [] }) })] }));
128
+ 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 ?? [] }) })] }));
66
129
  }
67
130
  // Mobile header component that uses ChatHeader context
68
- function MobileHeader({ agentName, showHeader, }) {
131
+ function MobileHeader({ agentName, showHeader, client, currentSessionId, }) {
69
132
  const { isExpanded, setIsExpanded } = ChatHeader.useChatHeaderContext();
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") }) })] }));
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") }) })] }));
71
134
  }
72
135
  // Header component that uses ChatLayout context (must be inside ChatLayout.Root)
73
- function AppChatHeader({ agentName, todos, sources, showHeader, sessionId, debuggerUrl, tools, mcps, subagents, }) {
136
+ function AppChatHeader({ agentName, todos, sources, showHeader, sessionId, debuggerUrl, tools, mcps, subagents, client, }) {
74
137
  const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
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: () => {
138
+ const debuggerLink = debuggerUrl
139
+ ? sessionId
140
+ ? `${debuggerUrl}/sessions/${sessionId}`
141
+ : debuggerUrl
142
+ : 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: () => {
77
144
  setPanelSize(panelSize === "hidden" ? "small" : "hidden");
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 ?? [] }) })] }) })] }));
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 ?? [] }) })] }) })] }));
79
146
  }
80
147
  export function ChatView({ client, initialSessionId, error: initError, debuggerUrl, }) {
81
148
  // Use shared hooks from @townco/ui/core - MUST be called before any early returns
@@ -95,6 +162,7 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
95
162
  const [agentSubagents, setAgentSubagents] = useState([]);
96
163
  const [isLargeScreen, setIsLargeScreen] = useState(typeof window !== "undefined" ? window.innerWidth >= 1024 : true);
97
164
  const [placeholder, setPlaceholder] = useState("Type a message or / for commands...");
165
+ const [hideTopBar, setHideTopBar] = useState(false);
98
166
  // Log connection status changes
99
167
  useEffect(() => {
100
168
  logger.debug("Connection status changed", { status: connectionStatus });
@@ -141,6 +209,9 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
141
209
  subagents: agentInfo.subagents,
142
210
  });
143
211
  }
212
+ if (agentInfo.uiConfig?.hideTopBar) {
213
+ setHideTopBar(true);
214
+ }
144
215
  }
145
216
  }, [client, sessionId, connectionStatus]);
146
217
  // Monitor screen size changes and update isLargeScreen state
@@ -220,7 +291,7 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
220
291
  },
221
292
  },
222
293
  ];
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) => {
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) => {
224
295
  sendMessage(prompt);
225
296
  setPlaceholder("Type a message or / for commands...");
226
297
  logger.info("Prompt clicked", { prompt });
@@ -232,7 +303,9 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
232
303
  const previousMessage = isFirst ? null : messages[index - 1];
233
304
  let spacingClass = "mt-2";
234
305
  if (isFirst) {
235
- spacingClass = "mt-2";
306
+ // First message needs more top margin if it's an assistant initial message
307
+ spacingClass =
308
+ message.role === "assistant" ? "mt-8" : "mt-2";
236
309
  }
237
310
  else if (message.role === "user") {
238
311
  // User message usually starts a new turn
@@ -35,7 +35,7 @@ export const PanelTabsHeader = React.forwardRef(({ showIcons = true, visibleTabs
35
35
  const gap = variant === "compact" ? "gap-[4px]" : "gap-3";
36
36
  return (_jsx(TabsList, { ref: ref, className: cn("w-full justify-start bg-transparent p-0 h-auto", gap, className), ...props, children: tabs.map((tab) => {
37
37
  const Icon = tab.icon;
38
- return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-paragraph-sm font-medium", "data-[state=active]:bg-muted data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground hover:text-foreground transition-colors"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
38
+ return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-paragraph-sm font-medium", "data-[state=active]:bg-accent data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground hover:text-foreground transition-colors"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
39
39
  }) }));
40
40
  });
41
41
  PanelTabsHeader.displayName = "PanelTabsHeader";
@@ -0,0 +1,27 @@
1
+ export interface SubAgentDetailsProps {
2
+ /** Sub-agent HTTP port */
3
+ port: number;
4
+ /** Sub-agent session ID */
5
+ sessionId: string;
6
+ /** Optional host (defaults to localhost) */
7
+ host?: string;
8
+ /** Parent tool call status - use this to determine if sub-agent is running */
9
+ parentStatus?: "pending" | "in_progress" | "completed" | "failed";
10
+ /** Sub-agent name (for display) */
11
+ agentName?: string | undefined;
12
+ /** Query sent to the sub-agent */
13
+ query?: string | undefined;
14
+ /** Controlled expanded state (optional - if not provided, uses internal state) */
15
+ isExpanded?: boolean;
16
+ /** Callback when expand state changes */
17
+ onExpandChange?: (expanded: boolean) => void;
18
+ }
19
+ /**
20
+ * SubAgentDetails component - displays streaming content from a sub-agent.
21
+ *
22
+ * This component:
23
+ * - Connects directly to the sub-agent's SSE endpoint
24
+ * - Displays streaming text and tool calls
25
+ * - Renders in a collapsible section (collapsed by default)
26
+ */
27
+ export declare function SubAgentDetails({ port, sessionId, host, parentStatus, agentName, query, isExpanded: controlledIsExpanded, onExpandChange, }: SubAgentDetailsProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,121 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDown, CircleDot, Loader2 } from "lucide-react";
3
+ import React, { useCallback, useEffect, useRef, useState } from "react";
4
+ import { useSubagentStream } from "../../core/hooks/use-subagent-stream.js";
5
+ const SCROLL_THRESHOLD = 50; // px from bottom to consider "at bottom"
6
+ /**
7
+ * SubAgentDetails component - displays streaming content from a sub-agent.
8
+ *
9
+ * This component:
10
+ * - Connects directly to the sub-agent's SSE endpoint
11
+ * - Displays streaming text and tool calls
12
+ * - Renders in a collapsible section (collapsed by default)
13
+ */
14
+ export function SubAgentDetails({ port, sessionId, host, parentStatus, agentName, query, isExpanded: controlledIsExpanded, onExpandChange, }) {
15
+ const [internalIsExpanded, setInternalIsExpanded] = useState(false);
16
+ // Use controlled state if provided, otherwise use internal state
17
+ const isExpanded = controlledIsExpanded ?? internalIsExpanded;
18
+ const setIsExpanded = onExpandChange ?? setInternalIsExpanded;
19
+ const [isThinkingExpanded, setIsThinkingExpanded] = useState(false);
20
+ const [isNearBottom, setIsNearBottom] = useState(true);
21
+ const thinkingContainerRef = useRef(null);
22
+ const { messages, isStreaming: hookIsStreaming, error, } = useSubagentStream({
23
+ port,
24
+ sessionId,
25
+ ...(host !== undefined ? { host } : {}),
26
+ });
27
+ // Use parent status as primary indicator, fall back to hook's streaming state
28
+ // Parent is "in_progress" means sub-agent is definitely still running
29
+ const isRunning = parentStatus === "in_progress" ||
30
+ parentStatus === "pending" ||
31
+ hookIsStreaming;
32
+ // Get the current/latest message
33
+ const currentMessage = messages[messages.length - 1];
34
+ const hasContent = currentMessage &&
35
+ (currentMessage.content ||
36
+ (currentMessage.toolCalls && currentMessage.toolCalls.length > 0) ||
37
+ (currentMessage.contentBlocks &&
38
+ currentMessage.contentBlocks.length > 0));
39
+ // Auto-collapse Thinking when completed (so Output is the primary view)
40
+ const prevIsRunningRef = useRef(isRunning);
41
+ useEffect(() => {
42
+ if (prevIsRunningRef.current && !isRunning) {
43
+ // Just completed - collapse thinking to show output
44
+ setIsThinkingExpanded(false);
45
+ }
46
+ prevIsRunningRef.current = isRunning;
47
+ }, [isRunning]);
48
+ // Check if user is near bottom of scroll area
49
+ const checkScrollPosition = useCallback(() => {
50
+ const container = thinkingContainerRef.current;
51
+ if (!container)
52
+ return;
53
+ const { scrollTop, scrollHeight, clientHeight } = container;
54
+ const distanceFromBottom = scrollHeight - scrollTop - clientHeight;
55
+ setIsNearBottom(distanceFromBottom < SCROLL_THRESHOLD);
56
+ }, []);
57
+ // Scroll to bottom
58
+ const scrollToBottom = useCallback(() => {
59
+ const container = thinkingContainerRef.current;
60
+ if (!container)
61
+ return;
62
+ container.scrollTop = container.scrollHeight;
63
+ }, []);
64
+ // Auto-scroll when content changes and user is near bottom
65
+ useEffect(() => {
66
+ if (isNearBottom && (isRunning || hasContent)) {
67
+ scrollToBottom();
68
+ }
69
+ }, [
70
+ currentMessage?.content,
71
+ currentMessage?.toolCalls,
72
+ isNearBottom,
73
+ isRunning,
74
+ hasContent,
75
+ scrollToBottom,
76
+ ]);
77
+ // Set up scroll listener
78
+ useEffect(() => {
79
+ const container = thinkingContainerRef.current;
80
+ if (!container)
81
+ return;
82
+ const handleScroll = () => checkScrollPosition();
83
+ container.addEventListener("scroll", handleScroll, { passive: true });
84
+ checkScrollPosition(); // Check initial position
85
+ return () => container.removeEventListener("scroll", handleScroll);
86
+ }, [checkScrollPosition, isThinkingExpanded, isExpanded]);
87
+ // Get last line of streaming content for preview
88
+ const lastLine = currentMessage?.content
89
+ ? currentMessage.content.split("\n").filter(Boolean).pop() || ""
90
+ : "";
91
+ const previewText = lastLine.length > 100 ? `${lastLine.slice(0, 100)}...` : lastLine;
92
+ // Get first line of query for fallback preview
93
+ const queryFirstLine = query
94
+ ? (query.split("\n")[0] ?? "").slice(0, 100) +
95
+ (query.length > 100 ? "..." : "")
96
+ : "";
97
+ return (_jsxs("div", { children: [!isExpanded && (_jsx("div", { className: "w-full max-w-md", children: previewText ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/70 truncate", children: previewText })) : queryFirstLine ? (_jsx("p", { className: "text-paragraph-sm text-muted-foreground/50 truncate", children: queryFirstLine })) : null })), isExpanded && (_jsxs("div", { className: "space-y-3", children: [(agentName || query) && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsxs("div", { className: "text-[11px] font-mono space-y-1", children: [agentName && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "agentName: " }), _jsx("span", { className: "text-foreground", children: agentName })] })), query && (_jsxs("div", { children: [_jsx("span", { className: "text-muted-foreground", children: "query: " }), _jsx("span", { className: "text-foreground", children: query })] }))] })] })), _jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => setIsThinkingExpanded(!isThinkingExpanded), className: "flex items-center gap-2 cursor-pointer bg-transparent border-none p-0 text-left group", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider font-sans", children: "Thinking" }), _jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isThinkingExpanded ? "rotate-180" : ""}` })] }), isThinkingExpanded && (_jsxs("div", { ref: thinkingContainerRef, className: "mt-2 rounded-md overflow-hidden bg-muted/30 border border-border/50 max-h-[200px] overflow-y-auto", children: [error && (_jsxs("div", { className: "px-2 py-2 text-[11px] text-destructive", children: ["Error: ", error] })), !error && !hasContent && isRunning && (_jsx("div", { className: "px-2 py-2 text-[11px] text-muted-foreground", children: "Waiting for sub-agent response..." })), currentMessage && (_jsxs("div", { className: "px-2 py-2 space-y-2", children: [currentMessage.contentBlocks &&
98
+ currentMessage.contentBlocks.length > 0
99
+ ? // Render interleaved content blocks
100
+ currentMessage.contentBlocks.map((block, idx) => block.type === "text" ? (_jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono", children: block.text }, `text-${idx}`)) : (_jsx(SubagentToolCallItem, { toolCall: block.toolCall }, block.toolCall.id)))
101
+ : // Fallback to legacy content
102
+ currentMessage.content && (_jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono", children: currentMessage.content })), currentMessage.isStreaming && (_jsx("span", { className: "inline-block w-1.5 h-3 bg-primary/70 ml-0.5 animate-pulse" }))] }))] }))] }), !isRunning && currentMessage?.content && (_jsxs("div", { children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsx("div", { className: "text-[11px] text-foreground whitespace-pre-wrap font-mono max-h-[200px] overflow-y-auto rounded-md bg-muted/30 border border-border/50 px-2 py-2", children: currentMessage.content })] }))] }))] }));
103
+ }
104
+ /**
105
+ * Simple tool call display for sub-agent tool calls
106
+ */
107
+ function SubagentToolCallItem({ toolCall }) {
108
+ const statusIcon = {
109
+ pending: "...",
110
+ in_progress: "",
111
+ completed: "",
112
+ failed: "",
113
+ }[toolCall.status];
114
+ const statusColor = {
115
+ pending: "text-muted-foreground",
116
+ in_progress: "text-blue-500",
117
+ completed: "text-green-500",
118
+ failed: "text-destructive",
119
+ }[toolCall.status];
120
+ return (_jsxs("div", { className: "flex items-center gap-2 text-[10px] text-muted-foreground", children: [_jsx("span", { className: statusColor, children: statusIcon }), _jsx("span", { className: "font-medium", children: toolCall.prettyName || toolCall.title }), toolCall.status === "in_progress" && (_jsx(Loader2, { className: "h-2.5 w-2.5 animate-spin" }))] }));
121
+ }
@@ -1,11 +1,21 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SquareCheckBig } from "lucide-react";
2
3
  import * as React from "react";
3
4
  import { cn } from "../lib/utils.js";
4
5
  import { TodoListItem } from "./TodoListItem.js";
6
+ /**
7
+ * Empty state component for the todo list
8
+ */
9
+ function TodoListEmptyState() {
10
+ return (_jsxs("div", { className: "flex flex-col items-center justify-center h-full gap-3", children: [_jsx(SquareCheckBig, { className: "size-8 text-neutral-300" }), _jsxs("p", { className: "text-base leading-6 text-neutral-400 text-center", children: ["There's nothing on the", _jsx("br", {}), "to-do list yet."] })] }));
11
+ }
5
12
  export const TodoList = React.forwardRef(({ client, todos, className, ...props }, ref) => {
6
13
  // For now, just use prop-based todos
7
14
  // Future: Add hook to get todos from store when available
8
15
  const todosToDisplay = todos || [];
9
- return (_jsx("div", { ref: ref, className: cn("space-y-2 max-h-64 overflow-y-auto", className), ...props, children: todosToDisplay.length === 0 ? (_jsx("p", { className: "text-paragraph-sm text-foreground opacity-60 italic", children: "No tasks yet." })) : (todosToDisplay.map((todo) => (_jsx(TodoListItem, { todo: todo }, todo.id)))) }));
16
+ if (todosToDisplay.length === 0) {
17
+ return (_jsx("div", { ref: ref, className: cn("h-full", className), ...props, children: _jsx(TodoListEmptyState, {}) }));
18
+ }
19
+ return (_jsx("div", { ref: ref, className: cn("space-y-2 max-h-64 overflow-y-auto", className), ...props, children: todosToDisplay.map((todo) => (_jsx(TodoListItem, { todo: todo }, todo.id))) }));
10
20
  });
11
21
  TodoList.displayName = "TodoList";
@@ -1,8 +1,9 @@
1
1
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
2
2
  import JsonView from "@uiw/react-json-view";
3
- import { AlertCircle, BrainCircuit, CheckSquare, ChevronDown, ChevronRight, Cloud, Edit, FileText, Globe, Image, Link, Search, Wrench, } from "lucide-react";
3
+ import { AlertCircle, CheckSquare, ChevronDown, ChevronRight, CircleDot, Cloud, Edit, FileText, Globe, Image, Link, Search, Wrench, } from "lucide-react";
4
4
  import React, { useState } from "react";
5
5
  import { ChatLayout } from "./index.js";
6
+ import { SubAgentDetails } from "./SubAgentDetails.js";
6
7
  import { useTheme } from "./ThemeProvider.js";
7
8
  /**
8
9
  * Map of icon names to Lucide components
@@ -17,7 +18,7 @@ const ICON_MAP = {
17
18
  FileText: FileText,
18
19
  Edit: Edit,
19
20
  Wrench: Wrench,
20
- BrainCircuit: BrainCircuit,
21
+ CircleDot: CircleDot,
21
22
  };
22
23
  /**
23
24
  * Tool call kind icons (using emoji for simplicity)
@@ -39,12 +40,14 @@ const _kindIcons = {
39
40
  */
40
41
  export function ToolCall({ toolCall }) {
41
42
  const [isExpanded, setIsExpanded] = useState(false);
43
+ const [isSubagentExpanded, setIsSubagentExpanded] = useState(false);
42
44
  const { resolvedTheme } = useTheme();
43
- // Detect TodoWrite tool
45
+ // Detect TodoWrite tool and subagent
44
46
  const isTodoWrite = toolCall.title === "todo_write";
47
+ const isSubagentCall = !!(toolCall.subagentPort && toolCall.subagentSessionId);
45
48
  // Safely access ChatLayout context - will be undefined if not within ChatLayout
46
49
  const layoutContext = React.useContext(ChatLayout.Context);
47
- // Click handler: toggle sidepanel for TodoWrite, expand for others
50
+ // Click handler: toggle sidepanel for TodoWrite, subagent details for subagents, expand for others
48
51
  const handleHeaderClick = React.useCallback(() => {
49
52
  if (isTodoWrite && layoutContext) {
50
53
  // Toggle sidepanel - close if already open on todo tab, otherwise open
@@ -57,17 +60,45 @@ export function ToolCall({ toolCall }) {
57
60
  layoutContext.setActiveTab("todo");
58
61
  }
59
62
  }
63
+ else if (isSubagentCall) {
64
+ // Toggle subagent details
65
+ setIsSubagentExpanded(!isSubagentExpanded);
66
+ }
60
67
  else {
61
68
  // Normal expand/collapse
62
69
  setIsExpanded(!isExpanded);
63
70
  }
64
- }, [isTodoWrite, layoutContext, isExpanded]);
71
+ }, [
72
+ isTodoWrite,
73
+ layoutContext,
74
+ isExpanded,
75
+ isSubagentCall,
76
+ isSubagentExpanded,
77
+ ]);
65
78
  // Determine which icon to show
66
79
  const IconComponent = toolCall.icon && ICON_MAP[toolCall.icon]
67
80
  ? ICON_MAP[toolCall.icon]
68
- : BrainCircuit;
81
+ : CircleDot;
69
82
  // Determine display name
70
83
  const displayName = toolCall.prettyName || toolCall.title;
84
+ // Determine icon color based on status (especially for subagents)
85
+ const isSubagentRunning = isSubagentCall &&
86
+ (toolCall.status === "in_progress" || toolCall.status === "pending");
87
+ const isSubagentFailed = isSubagentCall && toolCall.status === "failed";
88
+ const iconColorClass = isSubagentCall
89
+ ? isSubagentFailed
90
+ ? "text-destructive"
91
+ : isSubagentRunning
92
+ ? "text-foreground animate-pulse"
93
+ : "text-green-500"
94
+ : "text-muted-foreground";
95
+ const statusTooltip = isSubagentCall
96
+ ? isSubagentFailed
97
+ ? "Sub-agent failed"
98
+ : isSubagentRunning
99
+ ? "Sub-agent running"
100
+ : "Sub-agent completed"
101
+ : undefined;
71
102
  // Check if there's an error
72
103
  const hasError = toolCall.status === "failed" || !!toolCall.error;
73
104
  // Check if this is a preliminary (pending) tool call without full details yet
@@ -108,9 +139,11 @@ export function ToolCall({ toolCall }) {
108
139
  if (isPreliminary) {
109
140
  return (_jsx("div", { className: "flex flex-col my-4", children: _jsxs("span", { className: "text-paragraph-sm text-muted-foreground/50", children: ["Invoking ", displayName] }) }));
110
141
  }
111
- return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: "text-muted-foreground", children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: displayName }), hasError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground/70" })) : (_jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` }))] }), toolCall.subline && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: toolCall.subline }))] }), !isTodoWrite && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Files" }), _jsx("ul", { className: "space-y-1", children: toolCall.locations.map((loc) => (_jsxs("li", { className: "font-mono text-[11px] text-foreground bg-muted px-1.5 py-0.5 rounded w-fit", children: [loc.path, loc.line !== null &&
142
+ return (_jsxs("div", { className: "flex flex-col my-4", children: [_jsxs("button", { type: "button", className: "flex flex-col items-start gap-0.5 cursor-pointer bg-transparent border-none p-0 text-left group w-fit", onClick: handleHeaderClick, "aria-expanded": isTodoWrite ? undefined : isExpanded, children: [_jsxs("div", { className: "flex items-center gap-1.5 text-[11px] font-medium text-muted-foreground", children: [_jsx("div", { className: iconColorClass, title: statusTooltip, children: _jsx(IconComponent, { className: "h-3 w-3" }) }), _jsx("span", { className: "text-paragraph-sm text-muted-foreground", children: displayName }), hasError && _jsx(AlertCircle, { className: "h-3 w-3 text-destructive" }), isTodoWrite ? (_jsx(ChevronRight, { className: "h-3 w-3 text-muted-foreground/70" })) : (_jsx(ChevronDown, { className: `h-3 w-3 text-muted-foreground/70 transition-transform duration-200 ${isExpanded ? "rotate-180" : ""}` }))] }), toolCall.subline && (_jsx("span", { className: "text-paragraph-sm text-muted-foreground/70 pl-4.5", children: toolCall.subline }))] }), !isTodoWrite && isSubagentCall && (_jsx("div", { className: "pl-4.5", children: _jsx(SubAgentDetails, { port: toolCall.subagentPort, sessionId: toolCall.subagentSessionId, parentStatus: toolCall.status, agentName: toolCall.rawInput?.agentName, query: toolCall.rawInput?.query, isExpanded: isSubagentExpanded, onExpandChange: setIsSubagentExpanded }) })), !isTodoWrite && !isSubagentCall && isExpanded && (_jsxs("div", { className: "mt-2 text-sm border border-border rounded-lg bg-card overflow-hidden w-full", children: [toolCall.rawInput &&
143
+ Object.keys(toolCall.rawInput).length > 0 &&
144
+ !toolCall.subagentPort && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsx("div", { className: "text-[11px] font-mono text-foreground", children: _jsx(JsonView, { value: toolCall.rawInput, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) })] })), toolCall.locations && toolCall.locations.length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Files" }), _jsx("ul", { className: "space-y-1", children: toolCall.locations.map((loc) => (_jsxs("li", { className: "font-mono text-[11px] text-foreground bg-muted px-1.5 py-0.5 rounded w-fit", children: [loc.path, loc.line !== null &&
112
145
  loc.line !== undefined &&
113
- `:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), toolCall.rawInput && Object.keys(toolCall.rawInput).length > 0 && (_jsxs("div", { className: "p-3 border-b border-border", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Input" }), _jsx("div", { className: "text-[11px] font-mono text-foreground", children: _jsx(JsonView, { value: toolCall.rawInput, collapsed: false, displayDataTypes: false, displayObjectSize: false, enableClipboard: true, style: jsonStyle }) })] })), (toolCall.content && toolCall.content.length > 0) ||
146
+ `:${loc.line}`] }, `${loc.path}:${loc.line ?? ""}`))) })] })), (toolCall.content && toolCall.content.length > 0) ||
114
147
  toolCall.error ? (_jsxs("div", { className: "p-3 border-b border-border last:border-0", children: [_jsx("div", { className: "text-[10px] font-bold text-muted-foreground uppercase tracking-wider mb-1.5 font-sans", children: "Output" }), _jsxs("div", { className: "space-y-2 text-[11px] text-foreground", children: [toolCall.content?.map((block, idx) => {
115
148
  // Generate a stable key based on content
116
149
  const getBlockKey = () => {
@@ -34,6 +34,7 @@ export { Response, type ResponseProps } from "./Response.js";
34
34
  export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, } from "./Select.js";
35
35
  export { Toaster } from "./Sonner.js";
36
36
  export { type SourceItem, SourceListItem, type SourceListItemProps, } from "./SourceListItem.js";
37
+ export { SubAgentDetails, type SubAgentDetailsProps, } from "./SubAgentDetails.js";
37
38
  export { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs.js";
38
39
  export { Task, type TaskItem, TaskList, type TaskListProps, type TaskProps, } from "./Task.js";
39
40
  export { Textarea, type TextareaProps, textareaVariants } from "./Textarea.js";
@@ -40,6 +40,7 @@ export { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScro
40
40
  export { Toaster } from "./Sonner.js";
41
41
  // Toast components
42
42
  export { SourceListItem, } from "./SourceListItem.js";
43
+ export { SubAgentDetails, } from "./SubAgentDetails.js";
43
44
  export { Tabs, TabsContent, TabsList, TabsTrigger } from "./Tabs.js";
44
45
  // Task/Todo components
45
46
  export { Task, TaskList, } from "./Task.js";