@townco/ui 0.1.30 → 0.1.32

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.
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Export all core hooks
3
3
  */
4
+ export * from "./use-acp-client.js";
4
5
  export * from "./use-chat-input.js";
5
6
  export * from "./use-chat-messages.js";
6
7
  export * from "./use-chat-session.js";
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Export all core hooks
3
3
  */
4
+ export * from "./use-acp-client.js";
4
5
  export * from "./use-chat-input.js";
5
6
  export * from "./use-chat-messages.js";
6
7
  export * from "./use-chat-session.js";
@@ -0,0 +1,22 @@
1
+ import { AcpClient } from "../../sdk/client/index.js";
2
+ export interface UseACPClientOptions {
3
+ serverUrl: string;
4
+ }
5
+ export interface UseACPClientReturn {
6
+ client: AcpClient | null;
7
+ error: string | null;
8
+ sessionId: string | null;
9
+ }
10
+ /**
11
+ * Custom hook to initialize and manage an ACP client connection
12
+ *
13
+ * Handles:
14
+ * - Extracting session ID from URL query parameters
15
+ * - Creating and initializing the ACP client
16
+ * - Cleanup on unmount
17
+ * - Error handling
18
+ *
19
+ * @param options - Configuration options for the ACP client
20
+ * @returns Object containing client, error state, and session ID
21
+ */
22
+ export declare function useACPClient(options: UseACPClientOptions): UseACPClientReturn;
@@ -0,0 +1,63 @@
1
+ import { createLogger } from "@townco/core";
2
+ import { useEffect, useState } from "react";
3
+ import { AcpClient } from "../../sdk/client/index.js";
4
+ const logger = createLogger("acp-client-hook");
5
+ /**
6
+ * Custom hook to initialize and manage an ACP client connection
7
+ *
8
+ * Handles:
9
+ * - Extracting session ID from URL query parameters
10
+ * - Creating and initializing the ACP client
11
+ * - Cleanup on unmount
12
+ * - Error handling
13
+ *
14
+ * @param options - Configuration options for the ACP client
15
+ * @returns Object containing client, error state, and session ID
16
+ */
17
+ export function useACPClient(options) {
18
+ const [client, setClient] = useState(null);
19
+ const [error, setError] = useState(null);
20
+ const [sessionId, setSessionId] = useState(null);
21
+ useEffect(() => {
22
+ // Extract session ID from URL query parameter (?session=abc123)
23
+ const urlParams = new URLSearchParams(window.location.search);
24
+ const urlSessionId = urlParams.get("session");
25
+ if (urlSessionId) {
26
+ logger.info("Session ID found in URL", { sessionId: urlSessionId });
27
+ setSessionId(urlSessionId);
28
+ }
29
+ // Create AcpClient with HTTP transport
30
+ try {
31
+ logger.info("Initializing ACP client", {
32
+ serverUrl: options.serverUrl,
33
+ });
34
+ const acpClient = new AcpClient({
35
+ type: "http",
36
+ options: {
37
+ baseUrl: options.serverUrl,
38
+ },
39
+ });
40
+ setClient(acpClient);
41
+ logger.info("ACP client initialized successfully");
42
+ // Clean up on unmount
43
+ return () => {
44
+ logger.debug("Disconnecting ACP client");
45
+ acpClient.disconnect().catch((err) => {
46
+ logger.error("Failed to disconnect ACP client", {
47
+ error: err instanceof Error ? err.message : String(err),
48
+ });
49
+ });
50
+ };
51
+ }
52
+ catch (err) {
53
+ const errorMessage = err instanceof Error ? err.message : "Failed to initialize ACP client";
54
+ setError(errorMessage);
55
+ logger.error("Failed to initialize ACP client", {
56
+ error: err instanceof Error ? err.message : String(err),
57
+ stack: err instanceof Error ? err.stack : undefined,
58
+ });
59
+ return undefined;
60
+ }
61
+ }, [options.serverUrl]);
62
+ return { client, error, sessionId };
63
+ }
@@ -0,0 +1,7 @@
1
+ import type { AcpClient } from "../../sdk/client/index.js";
2
+ export interface ChatViewProps {
3
+ client: AcpClient | null;
4
+ initialSessionId?: string | null;
5
+ error?: string | null;
6
+ }
7
+ export declare function ChatView({ client, initialSessionId, error: initError, }: ChatViewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,153 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createLogger } from "@townco/core";
3
+ import { ArrowUp, ChevronUp, Code, PanelRight, Settings, Sparkles, } from "lucide-react";
4
+ import { useEffect, useState } from "react";
5
+ import { useChatMessages, useChatSession, useToolCalls, } from "../../core/hooks/index.js";
6
+ import { useChatStore } from "../../core/store/chat-store.js";
7
+ import { cn } from "../lib/utils.js";
8
+ import { ChatEmptyState, ChatHeader, ChatInputActions, ChatInputAttachment, ChatInputCommandMenu, ChatInputField, ChatInputRoot, ChatInputSubmit, ChatInputToolbar, ChatInputVoiceInput, ChatLayout, FilesTabContent, Message, MessageContent, PanelTabsHeader, SourcesTabContent, Tabs, TabsContent, TodoTabContent, } from "./index.js";
9
+ const logger = createLogger("gui");
10
+ // Mobile header component that uses ChatHeader context
11
+ function MobileHeader({ agentName }) {
12
+ const { isExpanded, setIsExpanded } = ChatHeader.useChatHeaderContext();
13
+ return (_jsxs("div", { className: "flex lg:hidden items-center gap-2 flex-1", children: [_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx("h1", { className: "text-[20px] font-semibold leading-[1.2] tracking-[-0.4px] text-foreground", children: agentName }), _jsx("div", { className: "flex items-center justify-center shrink-0", children: _jsx(ChevronUp, { className: "size-4 rotate-180 text-muted-foreground" }) })] }), _jsx("button", { type: "button", className: "flex items-center justify-center shrink-0 cursor-pointer", "aria-label": "Toggle menu", onClick: () => setIsExpanded(!isExpanded), children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }));
14
+ }
15
+ // Header component that uses ChatLayout context (must be inside ChatLayout.Root)
16
+ function AppChatHeader({ agentName, todos, sources, showHeader, }) {
17
+ const { panelSize, setPanelSize } = ChatLayout.useChatLayoutContext();
18
+ 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 gap-2 w-full h-16 py-5 pl-6 pr-4", children: [showHeader && (_jsxs("div", { className: "flex items-center gap-2 flex-1", children: [_jsx("h1", { className: "text-[20px] font-semibold leading-[1.2] tracking-[-0.4px] text-foreground", children: agentName }), _jsx("div", { className: "flex items-center justify-center shrink-0", children: _jsx(ChevronUp, { className: "size-4 rotate-180 text-muted-foreground" }) })] })), !showHeader && _jsx("div", { className: "flex-1" }), _jsx("button", { type: "button", className: "flex items-center justify-center shrink-0 cursor-pointer", "aria-label": "Toggle sidebar", onClick: () => {
19
+ setPanelSize(panelSize === "hidden" ? "small" : "hidden");
20
+ }, children: _jsx(PanelRight, { className: "size-4 text-muted-foreground" }) })] }), _jsx(MobileHeader, { agentName: agentName }), _jsx(ChatHeader.ExpandablePanel, { className: cn("pt-6 pb-8 px-6", "border-b border-border bg-card", "shadow-[0_4px_16px_0_rgba(0,0,0,0.04)]", "[border-bottom-width:0.5px]"), children: _jsxs(Tabs, { defaultValue: "todo", className: "w-full", children: [_jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources"], variant: "default" }), _jsx(TabsContent, { value: "todo", className: "mt-4", children: _jsx(TodoTabContent, { todos: todos }) }), _jsx(TabsContent, { value: "files", className: "mt-4", children: _jsx(FilesTabContent, {}) }), _jsx(TabsContent, { value: "sources", className: "mt-4", children: _jsx(SourcesTabContent, { sources: sources }) })] }) })] }));
21
+ }
22
+ export function ChatView({ client, initialSessionId, error: initError, }) {
23
+ // Use shared hooks from @townco/ui/core - MUST be called before any early returns
24
+ const { connectionStatus, connect, sessionId } = useChatSession(client, initialSessionId);
25
+ const { messages } = useChatMessages(client);
26
+ useToolCalls(client); // Still need to subscribe to tool call events
27
+ const error = useChatStore((state) => state.error);
28
+ const [agentName, setAgentName] = useState("Agent");
29
+ const [isLargeScreen, setIsLargeScreen] = useState(typeof window !== "undefined" ? window.innerWidth >= 1024 : true);
30
+ // Log connection status changes
31
+ useEffect(() => {
32
+ logger.debug("Connection status changed", { status: connectionStatus });
33
+ if (connectionStatus === "error" && error) {
34
+ logger.error("Connection error occurred", { error });
35
+ }
36
+ }, [connectionStatus, error]);
37
+ // Get agent name from session metadata
38
+ useEffect(() => {
39
+ if (client && sessionId) {
40
+ const session = client.getCurrentSession();
41
+ if (session?.metadata?.agentName) {
42
+ setAgentName(session.metadata.agentName);
43
+ }
44
+ }
45
+ }, [client, sessionId]);
46
+ // Monitor screen size changes and update isLargeScreen state
47
+ useEffect(() => {
48
+ const mediaQuery = window.matchMedia("(min-width: 1024px)");
49
+ const handleChange = (e) => {
50
+ setIsLargeScreen(e.matches);
51
+ };
52
+ // Set initial value
53
+ setIsLargeScreen(mediaQuery.matches);
54
+ // Listen for changes
55
+ mediaQuery.addEventListener("change", handleChange);
56
+ return () => {
57
+ mediaQuery.removeEventListener("change", handleChange);
58
+ };
59
+ }, []);
60
+ // If there's an initialization error, show error UI (after all hooks have been called)
61
+ if (initError) {
62
+ return (_jsx("div", { className: "flex items-center justify-center h-screen bg-background", children: _jsxs("div", { className: "text-center p-8 max-w-md", children: [_jsx("h1", { className: "text-2xl font-bold text-destructive mb-4", children: "Initialization Error" }), _jsx("p", { className: "text-foreground mb-4", children: initError }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Failed to initialize the ACP client. Check the console for details." })] }) }));
63
+ }
64
+ // TODO: Replace with useChatStore((state) => state.todos) when todos are added to the store
65
+ const todos = [];
66
+ // Dummy sources data based on Figma design
67
+ const sources = [
68
+ {
69
+ id: "1",
70
+ title: "Boeing Scores Early Wins at Dubai Airshow",
71
+ sourceName: "Reuters",
72
+ url: "https://www.reuters.com/markets/companies/BA.N",
73
+ snippet: "DUBAI, Nov 17 (Reuters) - Boeing (BA.N), opens new tab took centre stage at day one of the Dubai Airshow on Monday, booking a $38 billion order from host carrier Emirates and clinching more deals with African carriers, while China displayed its C919 in the Middle East for the first time.",
74
+ favicon: "https://www.google.com/s2/favicons?domain=reuters.com&sz=32",
75
+ },
76
+ {
77
+ id: "2",
78
+ title: "Boeing's Sustainable Aviation Goals Take Flight",
79
+ sourceName: "Forbes",
80
+ url: "https://www.forbes.com",
81
+ snippet: "SEATTLE, Nov 18 (Reuters) - Boeing is making headway towards its sustainability targets, unveiling plans for a new eco-friendly aircraft design aimed at reducing emissions by 50% by 2030.",
82
+ favicon: "https://www.google.com/s2/favicons?domain=forbes.com&sz=32",
83
+ },
84
+ {
85
+ id: "3",
86
+ title: "Boeing Faces Increased Competition in Global Aviation Market",
87
+ sourceName: "Reuters",
88
+ url: "https://www.reuters.com",
89
+ snippet: "CHICAGO, Nov 19 (Reuters) - As the global aviation industry rebounds post-pandemic, Boeing is grappling with intensified competition from rival manufacturers, particularly in the Asian market.",
90
+ favicon: "https://www.google.com/s2/favicons?domain=reuters.com&sz=32",
91
+ },
92
+ {
93
+ id: "4",
94
+ title: "Boeing's Starliner Successfully Completes Orbital Test Flight",
95
+ sourceName: "The Verge",
96
+ url: "https://www.theverge.com",
97
+ snippet: "NASA, Nov 20 (Reuters) - Boeing's CST-100 Starliner spacecraft achieves a significant milestone, successfully completing its orbital test flight, paving the way for future crewed missions to the International Space Station.",
98
+ favicon: "https://www.google.com/s2/favicons?domain=theverge.com&sz=32",
99
+ },
100
+ ];
101
+ // Command menu items for chat input
102
+ const commandMenuItems = [
103
+ {
104
+ id: "model-sonnet",
105
+ label: "Use Sonnet 4.5",
106
+ description: "Switch to Claude Sonnet 4.5 model",
107
+ icon: _jsx(Sparkles, { className: "h-4 w-4" }),
108
+ category: "model",
109
+ onSelect: () => {
110
+ logger.info("User selected Sonnet 4.5 model");
111
+ },
112
+ },
113
+ {
114
+ id: "model-opus",
115
+ label: "Use Opus",
116
+ description: "Switch to Claude Opus model",
117
+ icon: _jsx(Sparkles, { className: "h-4 w-4" }),
118
+ category: "model",
119
+ onSelect: () => {
120
+ logger.info("User selected Opus model");
121
+ },
122
+ },
123
+ {
124
+ id: "settings",
125
+ label: "Open Settings",
126
+ description: "Configure chat preferences",
127
+ icon: _jsx(Settings, { className: "h-4 w-4" }),
128
+ category: "action",
129
+ onSelect: () => {
130
+ logger.info("User opened settings");
131
+ },
132
+ },
133
+ {
134
+ id: "code-mode",
135
+ label: "Code Mode",
136
+ description: "Enable code-focused responses",
137
+ icon: _jsx(Code, { className: "h-4 w-4" }),
138
+ category: "mode",
139
+ onSelect: () => {
140
+ logger.info("User enabled code mode");
141
+ },
142
+ },
143
+ ];
144
+ return (_jsxs(ChatLayout.Root, { defaultPanelSize: "hidden", defaultActiveTab: "todo", children: [_jsxs(ChatLayout.Main, { children: [_jsx(AppChatHeader, { agentName: agentName, todos: todos, sources: sources, showHeader: messages.length > 0 }), connectionStatus === "error" && error && (_jsx("div", { className: "border-b border-destructive/20 bg-destructive/10 px-6 py-4", children: _jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "flex-1", children: [_jsx("h3", { className: "mb-1 text-sm font-semibold text-destructive", children: "Connection Error" }), _jsx("p", { className: "whitespace-pre-line text-sm text-foreground", children: error })] }), _jsx("button", { type: "button", onClick: connect, className: "rounded-lg bg-destructive px-4 py-2 text-sm font-medium text-destructive-foreground transition-colors hover:bg-destructive-hover", children: "Retry" })] }) })), _jsxs(ChatLayout.Body, { children: [_jsx(ChatLayout.Messages, { className: messages.length > 0 ? "pt-4" : "", children: messages.length === 0 ? (_jsx("div", { className: "flex flex-1 items-center px-4", children: _jsx(ChatEmptyState, { title: agentName, description: "This agent can help you with your tasks. Start a conversation by typing a message below.", suggestedPrompts: [
145
+ "Help me debug this code",
146
+ "Explain how this works",
147
+ "Create a new feature",
148
+ "Review my changes",
149
+ ], onPromptClick: (prompt) => {
150
+ // TODO: Implement prompt click handler
151
+ logger.info("Prompt clicked", { prompt });
152
+ } }) })) : (_jsx("div", { className: "flex flex-col gap-4 px-4", children: messages.map((message, index) => (_jsx(Message, { message: message, isLastMessage: index === messages.length - 1, children: _jsx(MessageContent, { message: message, thinkingDisplayStyle: "collapsible" }) }, message.id))) })) }), _jsx(ChatLayout.Footer, { children: _jsxs(ChatInputRoot, { client: client, children: [_jsx(ChatInputCommandMenu, { commands: commandMenuItems }), _jsx(ChatInputField, { placeholder: "Type a message or / for commands...", autoFocus: true }), _jsxs(ChatInputToolbar, { children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputActions, {}), _jsx(ChatInputAttachment, {})] }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsx(ChatInputVoiceInput, {}), _jsx(ChatInputSubmit, { children: _jsx(ArrowUp, { className: "size-4" }) })] })] })] }) })] })] }), isLargeScreen && (_jsx(ChatLayout.Aside, { breakpoint: "lg", children: _jsxs(Tabs, { defaultValue: "todo", className: "flex flex-col h-full", children: [_jsx("div", { className: cn("border-b border-border bg-card", "px-6 py-2 h-16", "flex items-center", "[border-bottom-width:0.5px]"), children: _jsx(PanelTabsHeader, { showIcons: true, visibleTabs: ["todo", "files", "sources"], variant: "compact" }) }), _jsx(TabsContent, { value: "todo", className: "flex-1 p-4 mt-0", children: _jsx(TodoTabContent, { 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, { sources: sources }) })] }) }))] }));
153
+ }
@@ -10,6 +10,7 @@ export { DatabaseTabContent, type DatabaseTabContentProps, FilesTabContent, type
10
10
  export { ChatSecondaryPanel, type ChatSecondaryPanelProps, } from "./ChatSecondaryPanel.js";
11
11
  export * as ChatSidebar from "./ChatSidebar.js";
12
12
  export { ChatStatus, type ChatStatusProps } from "./ChatStatus.js";
13
+ export { ChatView, type ChatViewProps } from "./ChatView.js";
13
14
  export { Conversation, type ConversationProps } from "./Conversation.js";
14
15
  export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, } from "./Dialog.js";
15
16
  export { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "./DropdownMenu.js";
@@ -12,6 +12,7 @@ export { DatabaseTabContent, FilesTabContent, SourcesTabContent, TodoTabContent,
12
12
  export { ChatSecondaryPanel, } from "./ChatSecondaryPanel.js";
13
13
  export * as ChatSidebar from "./ChatSidebar.js";
14
14
  export { ChatStatus } from "./ChatStatus.js";
15
+ export { ChatView } from "./ChatView.js";
15
16
  // Chat components - shadcn.io/ai inspired primitives
16
17
  export { Conversation } from "./Conversation.js";
17
18
  // Dialog components
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.30",
3
+ "version": "0.1.32",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@agentclientprotocol/sdk": "^0.5.1",
43
- "@townco/core": "0.0.8",
43
+ "@townco/core": "0.0.10",
44
44
  "@radix-ui/react-dialog": "^1.1.15",
45
45
  "@radix-ui/react-dropdown-menu": "^2.1.16",
46
46
  "@radix-ui/react-label": "^2.1.8",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "devDependencies": {
64
64
  "@tailwindcss/postcss": "^4.1.17",
65
- "@townco/tsconfig": "0.1.27",
65
+ "@townco/tsconfig": "0.1.29",
66
66
  "@types/node": "^24.10.0",
67
67
  "@types/react": "^19.2.2",
68
68
  "ink": "^6.4.0",