@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.
- package/dist/core/hooks/index.d.ts +1 -0
- package/dist/core/hooks/index.js +1 -0
- package/dist/core/hooks/use-chat-messages.d.ts +84 -0
- package/dist/core/hooks/use-chat-session.js +20 -3
- package/dist/core/hooks/use-subagent-stream.d.ts +28 -0
- package/dist/core/hooks/use-subagent-stream.js +254 -0
- package/dist/core/hooks/use-tool-calls.d.ts +84 -0
- package/dist/core/schemas/chat.d.ts +188 -0
- package/dist/core/schemas/tool-call.d.ts +286 -0
- package/dist/core/schemas/tool-call.js +53 -0
- package/dist/gui/components/ChatEmptyState.d.ts +2 -0
- package/dist/gui/components/ChatEmptyState.js +2 -2
- package/dist/gui/components/ChatLayout.d.ts +2 -0
- package/dist/gui/components/ChatLayout.js +70 -1
- package/dist/gui/components/ChatPanelTabContent.js +2 -2
- package/dist/gui/components/ChatSecondaryPanel.js +1 -1
- package/dist/gui/components/ChatView.js +85 -12
- package/dist/gui/components/PanelTabsHeader.js +1 -1
- package/dist/gui/components/SubAgentDetails.d.ts +27 -0
- package/dist/gui/components/SubAgentDetails.js +121 -0
- package/dist/gui/components/TodoList.js +12 -2
- package/dist/gui/components/ToolCall.js +41 -8
- package/dist/gui/components/index.d.ts +1 -0
- package/dist/gui/components/index.js +1 -0
- package/dist/sdk/client/acp-client.d.ts +9 -1
- package/dist/sdk/client/acp-client.js +10 -0
- package/dist/sdk/schemas/message.d.ts +2 -2
- package/dist/sdk/schemas/session.d.ts +96 -0
- package/dist/sdk/transports/http.d.ts +12 -1
- package/dist/sdk/transports/http.js +77 -1
- package/dist/sdk/transports/stdio.d.ts +3 -0
- package/dist/sdk/transports/types.d.ts +34 -0
- 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("
|
|
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-
|
|
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-
|
|
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(
|
|
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 =
|
|
76
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
}, [
|
|
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
|
-
:
|
|
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:
|
|
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 ?? ""}`))) })] })),
|
|
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";
|