@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.
@@ -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("space-y-2", className), ...props, children: _jsx(TodoList, { todos: todos }) }));
9
+ return (_jsx("div", { ref: ref, className: cn("h-full", className), ...props, children: _jsx(TodoList, { todos: todos, className: "h-full" }) }));
10
10
  });
11
11
  TodoTabContent.displayName = "TodoTabContent";
12
12
  export const FilesTabContent = React.forwardRef(({ files = [], provider, onFileSelect, className, ...props }, ref) => {
@@ -42,6 +42,6 @@ export const DatabaseTabContent = React.forwardRef(({ data, className, ...props
42
42
  });
43
43
  DatabaseTabContent.displayName = "DatabaseTabContent";
44
44
  export const SettingsTabContent = React.forwardRef(({ tools = [], mcps = [], subagents = [], className, ...props }, ref) => {
45
- return (_jsxs("div", { ref: ref, className: cn("space-y-6", className), ...props, children: [_jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Tools" }), tools.length > 0 ? (_jsx("div", { className: "space-y-2", children: tools.map((tool) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: tool.prettyName || tool.name }), tool.description && (_jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-1", children: tool.description }))] }, tool.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No tools available" }))] }), _jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "MCP Servers" }), mcps.length > 0 ? (_jsx("div", { className: "space-y-2", children: mcps.map((mcp) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: mcp.name }), _jsxs("div", { className: "text-caption text-muted-foreground mt-1", children: ["Transport: ", mcp.transport] })] }, mcp.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No MCP servers connected" }))] }), _jsxs("div", { className: "space-y-3", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Subagents" }), subagents.length > 0 ? (_jsx("div", { className: "space-y-2", children: subagents.map((subagent) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: subagent.name }), _jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-2", children: subagent.description })] }, subagent.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No subagents available" }))] })] }));
45
+ return (_jsxs("div", { ref: ref, className: cn("space-y-6", className), ...props, children: [_jsxs("div", { className: "space-y-3 p-2", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Tools" }), tools.length > 0 ? (_jsx("div", { className: "space-y-2", children: tools.map((tool) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: tool.prettyName || tool.name }), tool.description && (_jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-1", children: tool.description }))] }, tool.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No tools available" }))] }), _jsxs("div", { className: "space-y-3 p-2", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "MCP Servers" }), mcps.length > 0 ? (_jsx("div", { className: "space-y-2", children: mcps.map((mcp) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: mcp.name }), _jsxs("div", { className: "text-caption text-muted-foreground mt-1", children: ["Transport: ", mcp.transport] })] }, mcp.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No MCP servers connected" }))] }), _jsxs("div", { className: "space-y-3 p-2", children: [_jsx("h3", { className: "font-semibold text-subheading", children: "Subagents" }), subagents.length > 0 ? (_jsx("div", { className: "space-y-2", children: subagents.map((subagent) => (_jsxs("div", { className: "p-3 border border-border rounded-lg bg-muted/30", children: [_jsx("div", { className: "font-medium text-paragraph-sm", children: subagent.name }), _jsx("div", { className: "text-caption text-muted-foreground mt-1 line-clamp-2", children: subagent.description })] }, subagent.name))) })) : (_jsx("p", { className: "text-paragraph-sm text-muted-foreground", children: "No subagents available" }))] })] }));
46
46
  });
47
47
  SettingsTabContent.displayName = "SettingsTabContent";
@@ -60,7 +60,7 @@ export const ChatSecondaryPanel = React.forwardRef(({ client, todos, variant = "
60
60
  // Pills variant - Simple background highlight (Figma design)
61
61
  _jsx(TabsList, { className: cn("w-full justify-start bg-transparent p-0 h-auto", "gap-1"), children: tabs.map((tab) => {
62
62
  const Icon = tab.icon;
63
- return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-paragraph-sm font-medium", "data-[state=active]:bg-zinc-100 data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
63
+ return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-paragraph-sm font-medium", "data-[state=active]:bg-accent data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
64
64
  }) })) : (
65
65
  // Animated variant - Clip-path animation (original style)
66
66
  _jsxs("div", { className: "relative mb-4 border-border", children: [_jsx(TabsList, { className: "bg-transparent p-0 h-auto rounded-none w-full border-none", children: tabs.map((tab) => (_jsx(TabsTrigger, { value: tab.id, className: "px-3 py-1 text-paragraph-sm font-medium rounded-none text-foreground opacity-60 data-[state=active]:opacity-100 data-[state=active]:bg-transparent data-[state=active]:shadow-none", children: tab.label }, tab.id))) }), _jsx("div", { ref: containerRef, className: "absolute top-0 left-0 w-full overflow-hidden z-10 pointer-events-none", style: {
@@ -1,12 +1,12 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { createLogger } from "@townco/core";
3
- import { ArrowUp, Bug, ChevronUp, Code, PanelRight, Settings, Sparkles, X, } from "lucide-react";
4
- import { useEffect, useState } from "react";
3
+ import { ArrowUp, Bug, ChevronDown, ChevronUp, Code, PanelRight, Plus, Settings, Sparkles, X, } from "lucide-react";
4
+ import { useCallback, useEffect, useState } from "react";
5
5
  import { useChatMessages, useChatSession, useToolCalls, } from "../../core/hooks/index.js";
6
6
  import { selectTodosForCurrentSession, useChatStore, } from "../../core/store/chat-store.js";
7
7
  import { calculateTokenPercentage, formatTokenPercentage, } from "../../core/utils/model-context.js";
8
8
  import { cn } from "../lib/utils.js";
9
- import { ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, FilesTabContent, IconButton, Message, MessageContent, PanelTabsHeader, SettingsTabContent, SourcesTabContent, Tabs, TabsContent, ThemeToggle, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./index.js";
9
+ import { ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, ContextUsageButton, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, FilesTabContent, IconButton, Message, MessageContent, PanelTabsHeader, SettingsTabContent, SourcesTabContent, Tabs, TabsContent, ThemeToggle, TodoTabContent, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "./index.js";
10
10
  const logger = createLogger("gui");
11
11
  // Helper component to provide openFiles callback
12
12
  function OpenFilesButton({ children, }) {
@@ -47,6 +47,69 @@ function SidebarHotkey() {
47
47
  }, [panelSize, setPanelSize]);
48
48
  return null;
49
49
  }
50
+ // Format relative time from date string
51
+ function formatRelativeTime(dateString) {
52
+ const date = new Date(dateString);
53
+ const now = new Date();
54
+ const diffMs = now.getTime() - date.getTime();
55
+ const diffMins = Math.floor(diffMs / 60000);
56
+ const diffHours = Math.floor(diffMs / 3600000);
57
+ const diffDays = Math.floor(diffMs / 86400000);
58
+ if (diffMins < 1)
59
+ return "Just now";
60
+ if (diffMins < 60)
61
+ return `${diffMins}m ago`;
62
+ if (diffHours < 24)
63
+ return `${diffHours}h ago`;
64
+ if (diffDays < 7)
65
+ return `${diffDays}d ago`;
66
+ return date.toLocaleDateString();
67
+ }
68
+ // Session switcher dropdown component
69
+ function SessionSwitcher({ agentName, client, currentSessionId, }) {
70
+ const [sessions, setSessions] = useState([]);
71
+ const [isLoading, setIsLoading] = useState(false);
72
+ const [isOpen, setIsOpen] = useState(false);
73
+ const fetchSessions = useCallback(async () => {
74
+ if (!client)
75
+ return;
76
+ setIsLoading(true);
77
+ try {
78
+ const sessionList = await client.listSessions();
79
+ setSessions(sessionList);
80
+ }
81
+ catch (error) {
82
+ logger.error("Failed to fetch sessions", { error });
83
+ }
84
+ finally {
85
+ setIsLoading(false);
86
+ }
87
+ }, [client]);
88
+ // Fetch sessions when dropdown opens
89
+ useEffect(() => {
90
+ if (isOpen) {
91
+ fetchSessions();
92
+ }
93
+ }, [isOpen, fetchSessions]);
94
+ const handleNewSession = () => {
95
+ // Clear session from URL and reload to start fresh
96
+ const url = new URL(window.location.href);
97
+ url.searchParams.delete("session");
98
+ window.location.href = url.toString();
99
+ };
100
+ const handleSessionSelect = (sessionId) => {
101
+ if (sessionId === currentSessionId) {
102
+ setIsOpen(false);
103
+ return;
104
+ }
105
+ // Update URL with session ID and reload
106
+ const url = new URL(window.location.href);
107
+ url.searchParams.set("session", sessionId);
108
+ window.location.href = url.toString();
109
+ };
110
+ return (_jsxs(DropdownMenu, { open: isOpen, onOpenChange: setIsOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex items-center gap-1 text-heading-4 text-foreground hover:text-foreground/80 transition-colors cursor-pointer", children: [agentName, _jsx(ChevronDown, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isOpen && "rotate-180") })] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "w-72", children: [_jsxs(DropdownMenuLabel, { className: "flex items-center justify-between", children: [_jsx("span", { children: "Sessions" }), isLoading && (_jsx("span", { className: "text-caption text-muted-foreground", children: "Loading..." }))] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onClick: handleNewSession, className: "gap-2", children: [_jsx(Plus, { className: "size-4" }), _jsx("span", { children: "New Session" })] }), sessions.length > 0 && _jsx(DropdownMenuSeparator, {}), _jsx("div", { className: "max-h-64 overflow-y-auto", children: sessions.map((session) => (_jsxs(DropdownMenuItem, { onClick: () => handleSessionSelect(session.sessionId), className: cn("flex flex-col items-start gap-0.5 py-2", session.sessionId === currentSessionId &&
111
+ "bg-muted/50 font-medium"), children: [_jsxs("div", { className: "flex w-full items-center justify-between gap-2", children: [_jsx("span", { className: "truncate text-paragraph-sm", children: session.firstUserMessage || "Empty session" }), session.sessionId === currentSessionId && (_jsx("span", { className: "shrink-0 text-caption text-primary", children: "Current" }))] }), _jsxs("span", { className: "text-caption text-muted-foreground", children: [formatRelativeTime(session.updatedAt), " \u2022 ", session.messageCount, " ", "messages"] })] }, session.sessionId))) }), sessions.length === 0 && !isLoading && (_jsx("div", { className: "px-2 py-4 text-center text-paragraph-sm text-muted-foreground", children: "No previous sessions" }))] })] }));
112
+ }
50
113
  // Chat input with attachment handling
51
114
  function ChatInputWithAttachments({ client, startSession, placeholder, latestContextSize, currentModel, commandMenuItems, }) {
52
115
  const attachedFiles = useChatStore((state) => state.input.attachedFiles);
@@ -62,20 +125,24 @@ function ChatInputWithAttachments({ client, startSession, placeholder, latestCon
62
125
  // Controlled Tabs component for the aside panel
63
126
  function AsideTabs({ todos, tools, mcps, subagents, }) {
64
127
  const { activeTab, setActiveTab } = ChatLayout.useChatLayoutContext();
65
- return (_jsxs(Tabs, { value: activeTab, onValueChange: (value) => setActiveTab(value), className: "flex flex-col h-full", children: [_jsx("div", { className: cn("border-b border-border bg-card", "px-6 py-2 h-16", "flex items-center", "[border-bottom-width:0.5px]"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0", children: _jsx(SourcesTabContent, {}) }), _jsx(TabsContent, { value: "settings", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })] }));
128
+ return (_jsxs(Tabs, { value: activeTab, onValueChange: (value) => setActiveTab(value), className: "flex flex-col h-full", children: [_jsx("div", { className: cn("border-b border-border bg-card", "px-4 py-2 h-16", "flex items-center", "[border-bottom-width:0.5px]"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "flex-1 p-4 mt-0", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "flex-1 p-4 mt-0", children: _jsx(SourcesTabContent, {}) }), _jsx(TabsContent, { value: "settings", className: "flex-1 p-4 mt-0 overflow-y-auto", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })] }));
66
129
  }
67
130
  // Mobile header component that uses ChatHeader context
68
- function MobileHeader({ agentName, showHeader, }) {
131
+ function MobileHeader({ agentName, showHeader, client, currentSessionId, }) {
69
132
  const { isExpanded, setIsExpanded } = ChatHeader.useChatHeaderContext();
70
- return (_jsxs("div", { className: "flex lg:hidden items-center flex-1", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx("h1", { className: "text-heading-4 text-foreground", children: agentName }) })), !showHeader && _jsx("div", { className: "flex-1" }), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle menu", onClick: () => setIsExpanded(!isExpanded), children: _jsx(ChevronUp, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isExpanded ? "" : "rotate-180") }) })] }));
133
+ return (_jsxs("div", { className: "flex lg:hidden items-center flex-1", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx(SessionSwitcher, { agentName: agentName, client: client, currentSessionId: currentSessionId }) })), !showHeader && _jsx("div", { className: "flex-1" }), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle menu", onClick: () => setIsExpanded(!isExpanded), children: _jsx(ChevronUp, { className: cn("size-4 text-muted-foreground transition-transform duration-200", isExpanded ? "" : "rotate-180") }) })] }));
71
134
  }
72
135
  // Header component that uses ChatLayout context (must be inside ChatLayout.Root)
73
- function AppChatHeader({ agentName, todos, sources, showHeader, sessionId, debuggerUrl, tools, mcps, subagents, }) {
136
+ function AppChatHeader({ agentName, todos, sources, showHeader, sessionId, debuggerUrl, tools, mcps, subagents, client, }) {
74
137
  const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
75
- const debuggerLink = sessionId && debuggerUrl ? `${debuggerUrl}/sessions/${sessionId}` : null;
76
- return (_jsxs(ChatHeader.Root, { className: cn("border-b border-border bg-card relative lg:p-0", "[border-bottom-width:0.5px]"), children: [_jsxs("div", { className: "hidden lg:flex items-center w-full h-16 py-5 pl-6 pr-4", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx("h1", { className: "text-heading-4 text-foreground", children: agentName }) })), !showHeader && _jsx("div", { className: "flex-1" }), debuggerUrl && (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(IconButton, { "aria-label": "View session in debugger", disabled: !debuggerLink, asChild: !!debuggerLink, children: debuggerLink ? (_jsx("a", { href: debuggerLink, target: "_blank", rel: "noopener noreferrer", children: _jsx(Bug, { className: "size-4 text-muted-foreground" }) })) : (_jsx(Bug, { className: "size-4 text-muted-foreground" })) }) }), _jsx(TooltipContent, { children: _jsx("p", { children: "View session in debugger" }) })] }) })), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle sidebar", onClick: () => {
138
+ const debuggerLink = debuggerUrl
139
+ ? sessionId
140
+ ? `${debuggerUrl}/sessions/${sessionId}`
141
+ : debuggerUrl
142
+ : null;
143
+ return (_jsxs(ChatHeader.Root, { className: cn("border-b border-border bg-card relative lg:p-0", "[border-bottom-width:0.5px]"), children: [_jsxs("div", { className: "hidden lg:flex items-center w-full h-16 py-5 pl-6 pr-4", children: [showHeader && (_jsx("div", { className: "flex items-center gap-2 flex-1", children: _jsx(SessionSwitcher, { agentName: agentName, client: client, currentSessionId: sessionId }) })), !showHeader && _jsx("div", { className: "flex-1" }), debuggerUrl && (_jsx(TooltipProvider, { children: _jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { asChild: true, children: _jsx(IconButton, { "aria-label": "View session in debugger", disabled: !debuggerLink, asChild: !!debuggerLink, children: debuggerLink ? (_jsx("a", { href: debuggerLink, target: "_blank", rel: "noopener noreferrer", children: _jsx(Bug, { className: "size-4 text-muted-foreground" }) })) : (_jsx(Bug, { className: "size-4 text-muted-foreground" })) }) }), _jsx(TooltipContent, { children: _jsx("p", { children: sessionId ? "View session in debugger" : "Open debugger" }) })] }) })), _jsx(ThemeToggle, {}), _jsx(IconButton, { "aria-label": "Toggle sidebar", onClick: () => {
77
144
  setPanelSize(panelSize === "hidden" ? "small" : "hidden");
78
- }, children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName, showHeader: showHeader }), _jsx(ChatHeader.ExpandablePanel, { className: cn("pt-6 pb-8 px-6", "border-b border-border bg-card", "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]", "[border-bottom-width:0.5px]"), children: _jsxs(Tabs, { defaultValue: "todo", className: "w-full", children: [_jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "default" }), _jsx(TabsContent, { value: "todo", className: "mt-4", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "mt-4", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "mt-4", children: _jsx(SourcesTabContent, { sources: sources }) }), _jsx(TabsContent, { value: "settings", className: "mt-4", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })] }) })] }));
145
+ }, children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName, showHeader: showHeader, client: client, currentSessionId: sessionId }), _jsx(ChatHeader.ExpandablePanel, { className: cn("pt-6 pb-8 px-6", "border-b border-border bg-card", "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]", "[border-bottom-width:0.5px]"), children: _jsxs(Tabs, { defaultValue: "todo", className: "w-full", children: [_jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources", "settings"], variant: "default" }), _jsx(TabsContent, { value: "todo", className: "mt-4", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "mt-4", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "mt-4", children: _jsx(SourcesTabContent, { sources: sources }) }), _jsx(TabsContent, { value: "settings", className: "mt-4", children: _jsx(SettingsTabContent, { tools: tools ?? [], mcps: mcps ?? [], subagents: subagents ?? [] }) })] }) })] }));
79
146
  }
80
147
  export function ChatView({ client, initialSessionId, error: initError, debuggerUrl, }) {
81
148
  // Use shared hooks from @townco/ui/core - MUST be called before any early returns
@@ -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-muted data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground hover:text-foreground transition-colors"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
38
+ return (_jsxs(TabsTrigger, { value: tab.id, className: cn("gap-2 px-3 py-1.5 rounded-lg text-paragraph-sm font-medium", "data-[state=active]:bg-accent data-[state=active]:text-foreground", "data-[state=inactive]:text-muted-foreground hover:text-foreground transition-colors"), children: [showIcons && Icon && _jsx(Icon, { className: "size-4" }), tab.label] }, tab.id));
39
39
  }) }));
40
40
  });
41
41
  PanelTabsHeader.displayName = "PanelTabsHeader";
@@ -1,11 +1,21 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SquareCheckBig } from "lucide-react";
2
3
  import * as React from "react";
3
4
  import { cn } from "../lib/utils.js";
4
5
  import { TodoListItem } from "./TodoListItem.js";
6
+ /**
7
+ * Empty state component for the todo list
8
+ */
9
+ function TodoListEmptyState() {
10
+ return (_jsxs("div", { className: "flex flex-col items-center justify-center h-full gap-3", children: [_jsx(SquareCheckBig, { className: "size-8 text-neutral-300" }), _jsxs("p", { className: "text-base leading-6 text-neutral-400 text-center", children: ["There's nothing on the", _jsx("br", {}), "to-do list yet."] })] }));
11
+ }
5
12
  export const TodoList = React.forwardRef(({ client, todos, className, ...props }, ref) => {
6
13
  // For now, just use prop-based todos
7
14
  // Future: Add hook to get todos from store when available
8
15
  const todosToDisplay = todos || [];
9
- return (_jsx("div", { ref: ref, className: cn("space-y-2 max-h-64 overflow-y-auto", className), ...props, children: todosToDisplay.length === 0 ? (_jsx("p", { className: "text-paragraph-sm text-foreground opacity-60 italic", children: "No tasks yet." })) : (todosToDisplay.map((todo) => (_jsx(TodoListItem, { todo: todo }, todo.id)))) }));
16
+ if (todosToDisplay.length === 0) {
17
+ return (_jsx("div", { ref: ref, className: cn("h-full", className), ...props, children: _jsx(TodoListEmptyState, {}) }));
18
+ }
19
+ return (_jsx("div", { ref: ref, className: cn("space-y-2 max-h-64 overflow-y-auto", className), ...props, children: todosToDisplay.map((todo) => (_jsx(TodoListItem, { todo: todo }, todo.id))) }));
10
20
  });
11
21
  TodoList.displayName = "TodoList";
@@ -1,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
- : BrainCircuit;
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.50",
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.28",
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.47",
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",