@townco/ui 0.1.50 → 0.1.51
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/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 +78 -11
- package/dist/gui/components/PanelTabsHeader.js +1 -1
- package/dist/gui/components/TodoList.js +12 -2
- package/dist/gui/components/ToolCall.js +3 -2
- package/dist/sdk/client/acp-client.d.ts +5 -1
- package/dist/sdk/client/acp-client.js +9 -0
- package/dist/sdk/schemas/message.d.ts +2 -2
- package/dist/sdk/transports/http.d.ts +9 -1
- package/dist/sdk/transports/http.js +38 -1
- package/dist/sdk/transports/types.d.ts +25 -0
- package/package.json +3 -3
|
@@ -2,6 +2,8 @@ import * as React from "react";
|
|
|
2
2
|
export interface ChatEmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
3
3
|
/** Agent name/title */
|
|
4
4
|
title: string;
|
|
5
|
+
/** Optional custom title element (e.g., with dropdown) - overrides title if provided */
|
|
6
|
+
titleElement?: React.ReactNode;
|
|
5
7
|
/** Agent description */
|
|
6
8
|
description: string;
|
|
7
9
|
/** Optional guide link URL */
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { ChevronRight } from "lucide-react";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { cn } from "../lib/utils.js";
|
|
5
|
-
export const ChatEmptyState = React.forwardRef(({ title, description, guideUrl, guideText = "Guide", suggestedPrompts = [], onPromptClick, onGuideClick, onOpenFiles, onOpenSettings, toolsAndMcpsCount, onPromptHover, onPromptLeave, className, ...props }, ref) => {
|
|
5
|
+
export const ChatEmptyState = React.forwardRef(({ title, titleElement, description, guideUrl, guideText = "Guide", suggestedPrompts = [], onPromptClick, onGuideClick, onOpenFiles, onOpenSettings, toolsAndMcpsCount, onPromptHover, onPromptLeave, className, ...props }, ref) => {
|
|
6
6
|
const handlePromptClick = (prompt) => {
|
|
7
7
|
onPromptClick?.(prompt);
|
|
8
8
|
};
|
|
@@ -17,6 +17,6 @@ export const ChatEmptyState = React.forwardRef(({ title, description, guideUrl,
|
|
|
17
17
|
for (let i = 0; i < suggestedPrompts.length; i += 2) {
|
|
18
18
|
promptRows.push(suggestedPrompts.slice(i, i + 2));
|
|
19
19
|
}
|
|
20
|
-
return (_jsxs("div", { ref: ref, className: cn("flex flex-col items-start", className), ...props, children: [_jsx("h3", { className: "text-heading-4 text-text-primary mb-6", children: title }), _jsx("p", { className: "text-subheading text-text-secondary max-w-prose mb-6", children: description }), (onOpenFiles || onOpenSettings) && (_jsxs("div", { className: "flex items-center gap-1 -ml-3 mb-6", children: [onOpenFiles && (_jsxs("button", { type: "button", onClick: onOpenFiles, className: "inline-flex items-center gap-1 py-1.5 pr-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: "View Files" }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] })), onOpenSettings && (_jsxs("button", { type: "button", onClick: onOpenSettings, className: "inline-flex items-center gap-1 py-1.5 pr-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsxs("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: ["View Tools & MCPs", toolsAndMcpsCount !== undefined && toolsAndMcpsCount > 0 && (_jsxs("span", { className: "ml-1", children: ["(", toolsAndMcpsCount, ")"] }))] }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] }))] })), (guideUrl || onGuideClick) && (_jsxs("button", { type: "button", onClick: handleGuideClick, className: "inline-flex items-center gap-1 py-1.5 pr-3 -ml-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: guideText }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] })), suggestedPrompts.length > 0 && (_jsxs("div", { className: "flex flex-col gap-3 w-full max-w-prompt-container", children: [_jsx("p", { className: "text-label text-text-tertiary", children: "Suggested Prompts" }), _jsx("div", { className: "flex flex-col gap-2.5", children: promptRows.map((row) => (_jsx("div", { className: "flex gap-2.5 items-center", children: row.map((prompt) => (_jsx("button", { type: "button", onClick: () => handlePromptClick(prompt), onMouseEnter: () => onPromptHover?.(prompt), onMouseLeave: () => onPromptLeave?.(), className: "flex-1 flex items-start gap-2 p-3 bg-secondary hover:bg-secondary/80 rounded-2xl transition-colors min-w-0", children: _jsx("span", { className: "text-paragraph font-normal leading-normal text-text-tertiary truncate", children: prompt }) }, prompt))) }, row.join("-")))) })] }))] }));
|
|
20
|
+
return (_jsxs("div", { ref: ref, className: cn("flex flex-col items-start", className), ...props, children: [titleElement ? (_jsx("div", { className: "text-heading-4 text-text-primary mb-6", children: titleElement })) : (_jsx("h3", { className: "text-heading-4 text-text-primary mb-6", children: title })), _jsx("p", { className: "text-subheading text-text-secondary max-w-prose mb-6", children: description }), (onOpenFiles || onOpenSettings) && (_jsxs("div", { className: "flex items-center gap-1 -ml-3 mb-6", children: [onOpenFiles && (_jsxs("button", { type: "button", onClick: onOpenFiles, className: "inline-flex items-center gap-1 py-1.5 pr-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: "View Files" }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] })), onOpenSettings && (_jsxs("button", { type: "button", onClick: onOpenSettings, className: "inline-flex items-center gap-1 py-1.5 pr-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsxs("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: ["View Tools & MCPs", toolsAndMcpsCount !== undefined && toolsAndMcpsCount > 0 && (_jsxs("span", { className: "ml-1", children: ["(", toolsAndMcpsCount, ")"] }))] }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] }))] })), (guideUrl || onGuideClick) && (_jsxs("button", { type: "button", onClick: handleGuideClick, className: "inline-flex items-center gap-1 py-1.5 pr-3 -ml-3 pl-3 rounded-lg hover:bg-accent transition-colors", children: [_jsx("span", { className: "text-paragraph-sm-medium text-foreground tracking-wide leading-none", children: guideText }), _jsx(ChevronRight, { className: "size-4 text-foreground shrink-0" })] })), suggestedPrompts.length > 0 && (_jsxs("div", { className: "flex flex-col gap-3 w-full max-w-prompt-container", children: [_jsx("p", { className: "text-label text-text-tertiary", children: "Suggested Prompts" }), _jsx("div", { className: "flex flex-col gap-2.5", children: promptRows.map((row) => (_jsx("div", { className: "flex gap-2.5 items-center", children: row.map((prompt) => (_jsx("button", { type: "button", onClick: () => handlePromptClick(prompt), onMouseEnter: () => onPromptHover?.(prompt), onMouseLeave: () => onPromptLeave?.(), className: "flex-1 flex items-start gap-2 p-3 bg-secondary hover:bg-secondary/80 rounded-2xl transition-colors min-w-0", children: _jsx("span", { className: "text-paragraph font-normal leading-normal text-text-tertiary truncate", children: prompt }) }, prompt))) }, row.join("-")))) })] }))] }));
|
|
21
21
|
});
|
|
22
22
|
ChatEmptyState.displayName = "ChatEmptyState";
|
|
@@ -36,6 +36,8 @@ export interface ChatLayoutMessagesProps extends React.HTMLAttributes<HTMLDivEle
|
|
|
36
36
|
onScrollChange?: (isAtBottom: boolean) => void;
|
|
37
37
|
/** Whether to show scroll to bottom button */
|
|
38
38
|
showScrollToBottom?: boolean;
|
|
39
|
+
/** Whether to scroll to bottom on initial mount (default: true) */
|
|
40
|
+
initialScrollToBottom?: boolean;
|
|
39
41
|
}
|
|
40
42
|
declare const ChatLayoutMessages: React.ForwardRefExoticComponent<ChatLayoutMessagesProps & React.RefAttributes<HTMLDivElement>>;
|
|
41
43
|
export interface ChatLayoutFooterProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -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
|
|
@@ -220,7 +287,7 @@ export function ChatView({ client, initialSessionId, error: initError, debuggerU
|
|
|
220
287
|
},
|
|
221
288
|
},
|
|
222
289
|
];
|
|
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) => {
|
|
290
|
+
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, 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
291
|
sendMessage(prompt);
|
|
225
292
|
setPlaceholder("Type a message or / for commands...");
|
|
226
293
|
logger.info("Prompt clicked", { prompt });
|
|
@@ -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";
|
|
@@ -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,6 +1,6 @@
|
|
|
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, BrainCircuit, 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
6
|
import { useTheme } from "./ThemeProvider.js";
|
|
@@ -18,6 +18,7 @@ const ICON_MAP = {
|
|
|
18
18
|
Edit: Edit,
|
|
19
19
|
Wrench: Wrench,
|
|
20
20
|
BrainCircuit: BrainCircuit,
|
|
21
|
+
CircleDot: CircleDot,
|
|
21
22
|
};
|
|
22
23
|
/**
|
|
23
24
|
* Tool call kind icons (using emoji for simplicity)
|
|
@@ -65,7 +66,7 @@ export function ToolCall({ toolCall }) {
|
|
|
65
66
|
// Determine which icon to show
|
|
66
67
|
const IconComponent = toolCall.icon && ICON_MAP[toolCall.icon]
|
|
67
68
|
? ICON_MAP[toolCall.icon]
|
|
68
|
-
:
|
|
69
|
+
: CircleDot;
|
|
69
70
|
// Determine display name
|
|
70
71
|
const displayName = toolCall.prettyName || toolCall.title;
|
|
71
72
|
// Check if there's an error
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MessageChunk, Session, SessionConfig, SessionUpdate } from "../schemas/index.js";
|
|
2
|
-
import type { HttpTransportOptions, StdioTransportOptions, WebSocketTransportOptions } from "../transports/index.js";
|
|
2
|
+
import type { HttpTransportOptions, SessionSummary, StdioTransportOptions, WebSocketTransportOptions } from "../transports/index.js";
|
|
3
3
|
/**
|
|
4
4
|
* Client configuration with explicit transport selection
|
|
5
5
|
*/
|
|
@@ -48,6 +48,10 @@ export declare class AcpClient {
|
|
|
48
48
|
* Load an existing session
|
|
49
49
|
*/
|
|
50
50
|
loadSession(sessionId: string, config?: Partial<SessionConfig>): Promise<string>;
|
|
51
|
+
/**
|
|
52
|
+
* List available sessions
|
|
53
|
+
*/
|
|
54
|
+
listSessions(): Promise<SessionSummary[]>;
|
|
51
55
|
/**
|
|
52
56
|
* Send a message in the current session
|
|
53
57
|
*/
|
|
@@ -131,6 +131,15 @@ export class AcpClient {
|
|
|
131
131
|
this.updateSessionStatus(sessionId, "connected");
|
|
132
132
|
return sessionId;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* List available sessions
|
|
136
|
+
*/
|
|
137
|
+
async listSessions() {
|
|
138
|
+
if (!this.transport.listSessions) {
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
return this.transport.listSessions();
|
|
142
|
+
}
|
|
134
143
|
/**
|
|
135
144
|
* Send a message in the current session
|
|
136
145
|
*/
|
|
@@ -13,9 +13,9 @@ export type MessageRole = z.infer<typeof MessageRole>;
|
|
|
13
13
|
* Content type for messages
|
|
14
14
|
*/
|
|
15
15
|
export declare const ContentType: z.ZodEnum<{
|
|
16
|
+
file: "file";
|
|
16
17
|
text: "text";
|
|
17
18
|
image: "image";
|
|
18
|
-
file: "file";
|
|
19
19
|
tool_call: "tool_call";
|
|
20
20
|
tool_result: "tool_result";
|
|
21
21
|
}>;
|
|
@@ -25,9 +25,9 @@ export type ContentType = z.infer<typeof ContentType>;
|
|
|
25
25
|
*/
|
|
26
26
|
export declare const BaseContent: z.ZodObject<{
|
|
27
27
|
type: z.ZodEnum<{
|
|
28
|
+
file: "file";
|
|
28
29
|
text: "text";
|
|
29
30
|
image: "image";
|
|
30
|
-
file: "file";
|
|
31
31
|
tool_call: "tool_call";
|
|
32
32
|
tool_result: "tool_result";
|
|
33
33
|
}>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Message, MessageChunk, SessionUpdate } from "../schemas/index.js";
|
|
2
|
-
import type { HttpTransportOptions, Transport } from "./types.js";
|
|
2
|
+
import type { HttpTransportOptions, SessionSummary, Transport } from "./types.js";
|
|
3
3
|
/**
|
|
4
4
|
* HTTP transport implementation using ACP over HTTP + SSE
|
|
5
5
|
* Uses POST /rpc for client->agent messages and GET /events (SSE) for agent->client
|
|
@@ -29,6 +29,10 @@ export declare class HttpTransport implements Transport {
|
|
|
29
29
|
* @param sessionId - The session ID to load
|
|
30
30
|
*/
|
|
31
31
|
loadSession(sessionId: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* List available sessions from the server
|
|
34
|
+
*/
|
|
35
|
+
listSessions(): Promise<SessionSummary[]>;
|
|
32
36
|
disconnect(): Promise<void>;
|
|
33
37
|
send(message: Message): Promise<void>;
|
|
34
38
|
receive(): AsyncIterableIterator<MessageChunk>;
|
|
@@ -41,6 +45,10 @@ export declare class HttpTransport implements Transport {
|
|
|
41
45
|
version?: string;
|
|
42
46
|
description?: string;
|
|
43
47
|
suggestedPrompts?: string[];
|
|
48
|
+
initialMessage?: {
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
content: string;
|
|
51
|
+
};
|
|
44
52
|
tools?: Array<{
|
|
45
53
|
name: string;
|
|
46
54
|
description?: string;
|
|
@@ -65,6 +65,12 @@ export class HttpTransport {
|
|
|
65
65
|
const subagents = metaIsObject && "subagents" in meta && Array.isArray(meta.subagents)
|
|
66
66
|
? meta.subagents
|
|
67
67
|
: undefined;
|
|
68
|
+
const initialMessage = metaIsObject &&
|
|
69
|
+
"initialMessage" in meta &&
|
|
70
|
+
meta.initialMessage &&
|
|
71
|
+
typeof meta.initialMessage === "object"
|
|
72
|
+
? meta.initialMessage
|
|
73
|
+
: undefined;
|
|
68
74
|
this.agentInfo = {
|
|
69
75
|
name: initResponse.agentInfo.name,
|
|
70
76
|
// title is the ACP field for human-readable display name
|
|
@@ -74,6 +80,7 @@ export class HttpTransport {
|
|
|
74
80
|
version: initResponse.agentInfo.version,
|
|
75
81
|
...(description ? { description } : {}),
|
|
76
82
|
...(suggestedPrompts ? { suggestedPrompts } : {}),
|
|
83
|
+
...(initialMessage ? { initialMessage } : {}),
|
|
77
84
|
...(tools ? { tools } : {}),
|
|
78
85
|
...(mcps ? { mcps } : {}),
|
|
79
86
|
...(subagents ? { subagents } : {}),
|
|
@@ -146,6 +153,12 @@ export class HttpTransport {
|
|
|
146
153
|
const subagents = metaIsObject && "subagents" in meta && Array.isArray(meta.subagents)
|
|
147
154
|
? meta.subagents
|
|
148
155
|
: undefined;
|
|
156
|
+
const initialMessage = metaIsObject &&
|
|
157
|
+
"initialMessage" in meta &&
|
|
158
|
+
meta.initialMessage &&
|
|
159
|
+
typeof meta.initialMessage === "object"
|
|
160
|
+
? meta.initialMessage
|
|
161
|
+
: undefined;
|
|
149
162
|
this.agentInfo = {
|
|
150
163
|
name: initResponse.agentInfo.name,
|
|
151
164
|
// title is the ACP field for human-readable display name
|
|
@@ -155,6 +168,7 @@ export class HttpTransport {
|
|
|
155
168
|
version: initResponse.agentInfo.version,
|
|
156
169
|
...(description ? { description } : {}),
|
|
157
170
|
...(suggestedPrompts ? { suggestedPrompts } : {}),
|
|
171
|
+
...(initialMessage ? { initialMessage } : {}),
|
|
158
172
|
...(tools ? { tools } : {}),
|
|
159
173
|
...(mcps ? { mcps } : {}),
|
|
160
174
|
...(subagents ? { subagents } : {}),
|
|
@@ -204,6 +218,29 @@ export class HttpTransport {
|
|
|
204
218
|
throw err;
|
|
205
219
|
}
|
|
206
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* List available sessions from the server
|
|
223
|
+
*/
|
|
224
|
+
async listSessions() {
|
|
225
|
+
try {
|
|
226
|
+
const fetchOptions = {
|
|
227
|
+
method: "GET",
|
|
228
|
+
};
|
|
229
|
+
if (this.options.headers) {
|
|
230
|
+
fetchOptions.headers = this.options.headers;
|
|
231
|
+
}
|
|
232
|
+
const response = await fetch(`${this.options.baseUrl}/sessions`, fetchOptions);
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
|
235
|
+
}
|
|
236
|
+
const data = await response.json();
|
|
237
|
+
return data.sessions || [];
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
logger.error("Failed to list sessions", { error });
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
207
244
|
async disconnect() {
|
|
208
245
|
if (!this.connected) {
|
|
209
246
|
return;
|
|
@@ -982,7 +1019,7 @@ export class HttpTransport {
|
|
|
982
1019
|
timestamp: new Date().toISOString(),
|
|
983
1020
|
},
|
|
984
1021
|
};
|
|
985
|
-
// Notify as a complete message (for session replay)
|
|
1022
|
+
// Notify as a complete message (for session replay or initial message)
|
|
986
1023
|
this.notifySessionUpdate(messageSessionUpdate);
|
|
987
1024
|
}
|
|
988
1025
|
}
|
|
@@ -22,6 +22,25 @@ export interface AgentSubagentInfo {
|
|
|
22
22
|
name: string;
|
|
23
23
|
description: string;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Initial message configuration for agents
|
|
27
|
+
*/
|
|
28
|
+
export interface AgentInitialMessage {
|
|
29
|
+
/** Whether the agent should send an initial message when a session starts */
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
/** The content of the initial message */
|
|
32
|
+
content: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Session summary for listing
|
|
36
|
+
*/
|
|
37
|
+
export interface SessionSummary {
|
|
38
|
+
sessionId: string;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
updatedAt: string;
|
|
41
|
+
messageCount: number;
|
|
42
|
+
firstUserMessage?: string;
|
|
43
|
+
}
|
|
25
44
|
/**
|
|
26
45
|
* Transport interface for different communication methods
|
|
27
46
|
*/
|
|
@@ -34,6 +53,10 @@ export interface Transport {
|
|
|
34
53
|
* Load an existing session (optional, not all transports support this)
|
|
35
54
|
*/
|
|
36
55
|
loadSession?(sessionId: string): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* List available sessions (optional, not all transports support this)
|
|
58
|
+
*/
|
|
59
|
+
listSessions?(): Promise<SessionSummary[]>;
|
|
37
60
|
/**
|
|
38
61
|
* Close the transport connection
|
|
39
62
|
*/
|
|
@@ -66,6 +89,7 @@ export interface Transport {
|
|
|
66
89
|
* - tools: List of tools available to the agent
|
|
67
90
|
* - mcps: List of MCP servers connected to the agent
|
|
68
91
|
* - subagents: List of subagents available via Task tool
|
|
92
|
+
* - initialMessage: Configuration for agent's initial message on session start
|
|
69
93
|
*/
|
|
70
94
|
getAgentInfo?(): {
|
|
71
95
|
name?: string;
|
|
@@ -73,6 +97,7 @@ export interface Transport {
|
|
|
73
97
|
version?: string;
|
|
74
98
|
description?: string;
|
|
75
99
|
suggestedPrompts?: string[];
|
|
100
|
+
initialMessage?: AgentInitialMessage;
|
|
76
101
|
tools?: AgentToolInfo[];
|
|
77
102
|
mcps?: AgentMcpInfo[];
|
|
78
103
|
subagents?: AgentSubagentInfo[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.51",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@agentclientprotocol/sdk": "^0.5.1",
|
|
47
|
-
"@townco/core": "0.0.
|
|
47
|
+
"@townco/core": "0.0.29",
|
|
48
48
|
"@radix-ui/react-dialog": "^1.1.15",
|
|
49
49
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
|
50
50
|
"@radix-ui/react-label": "^2.1.8",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
69
|
"@tailwindcss/postcss": "^4.1.17",
|
|
70
|
-
"@townco/tsconfig": "0.1.
|
|
70
|
+
"@townco/tsconfig": "0.1.48",
|
|
71
71
|
"@types/node": "^24.10.0",
|
|
72
72
|
"@types/react": "^19.2.2",
|
|
73
73
|
"ink": "^6.4.0",
|