@townco/gui-template 0.1.0

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.
@@ -0,0 +1,19 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Agent Chat</title>
7
+ <script type="module" crossorigin src="/assets/index-C9pfdbke.js"></script>
8
+ <link rel="modulepreload" crossorigin href="/assets/vendor-CNJ4Zu8U.js">
9
+ <link rel="modulepreload" crossorigin href="/assets/react-BUwpDnu9.js">
10
+ <link rel="modulepreload" crossorigin href="/assets/acp-sdk-DjnJtAf8.js">
11
+ <link rel="modulepreload" crossorigin href="/assets/radix-zBfH0NEp.js">
12
+ <link rel="modulepreload" crossorigin href="/assets/icons-B1wHwGRq.js">
13
+ <link rel="modulepreload" crossorigin href="/assets/markdown-BLVKlqjB.js">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-B6Vo9iGc.css">
15
+ </head>
16
+ <body>
17
+ <div id="root"></div>
18
+ </body>
19
+ </html>
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Agent Chat</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@townco/gui-template",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "files": [
6
+ "dist",
7
+ "src",
8
+ "index.html",
9
+ "vite.config.ts",
10
+ "postcss.config.js",
11
+ "README.md"
12
+ ],
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/federicoweber/agent_hub.git"
16
+ },
17
+ "author": "Federico Weber",
18
+ "scripts": {
19
+ "dev": "vite",
20
+ "build": "vite build",
21
+ "preview": "vite preview",
22
+ "check": "tsc --noEmit"
23
+ },
24
+ "dependencies": {
25
+ "@townco/ui": "^0.1.0",
26
+ "lucide-react": "^0.552.0",
27
+ "react": "^19.2.0",
28
+ "react-dom": "^19.2.0"
29
+ },
30
+ "devDependencies": {
31
+ "@tailwindcss/postcss": "^4.1.17",
32
+ "@townco/tsconfig": "^0.1.0",
33
+ "@types/react": "^19.2.2",
34
+ "@types/react-dom": "^19.2.2",
35
+ "@vitejs/plugin-react": "^5.1.0",
36
+ "autoprefixer": "^10.4.21",
37
+ "postcss": "^8.5.6",
38
+ "tailwindcss": "^4.1.17",
39
+ "typescript": "^5.9.3",
40
+ "vite": "^7.2.1"
41
+ }
42
+ }
@@ -0,0 +1,5 @@
1
+ export default {
2
+ plugins: {
3
+ "@tailwindcss/postcss": {},
4
+ },
5
+ };
package/src/App.tsx ADDED
@@ -0,0 +1,54 @@
1
+ import { AcpClient } from "@townco/ui";
2
+ import { useEffect, useState } from "react";
3
+ import { ChatView } from "./ChatView.js";
4
+ import { config } from "./config.js";
5
+
6
+ function App() {
7
+ const [client, setClient] = useState<AcpClient | null>(null);
8
+ const [error, setError] = useState<string | null>(null);
9
+
10
+ useEffect(() => {
11
+ // Create AcpClient with HTTP transport
12
+ try {
13
+ const acpClient = new AcpClient({
14
+ type: "http",
15
+ options: {
16
+ baseUrl: config.agentServerUrl,
17
+ },
18
+ });
19
+
20
+ setClient(acpClient);
21
+
22
+ // Clean up on unmount
23
+ return () => {
24
+ acpClient.disconnect().catch(console.error);
25
+ };
26
+ } catch (err) {
27
+ const errorMessage =
28
+ err instanceof Error ? err.message : "Failed to initialize ACP client";
29
+ setError(errorMessage);
30
+ console.error("Failed to initialize ACP client:", err);
31
+ return undefined;
32
+ }
33
+ }, []);
34
+
35
+ if (error) {
36
+ return (
37
+ <div className="flex items-center justify-center h-screen bg-[var(--color-bg)]">
38
+ <div className="text-center p-8 max-w-md">
39
+ <h1 className="text-2xl font-bold text-red-500 mb-4">
40
+ Initialization Error
41
+ </h1>
42
+ <p className="text-[var(--color-text)] mb-4">{error}</p>
43
+ <p className="text-sm text-[var(--color-text-secondary)]">
44
+ Failed to initialize the ACP client. Check the console for details.
45
+ </p>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
50
+
51
+ return <ChatView client={client} />;
52
+ }
53
+
54
+ export default App;
@@ -0,0 +1,173 @@
1
+ import {
2
+ type AcpClient,
3
+ useChatMessages,
4
+ useChatSession,
5
+ useChatStore,
6
+ } from "@townco/ui";
7
+ import {
8
+ ChatInputField,
9
+ ChatInputRoot,
10
+ ChatInputSubmit,
11
+ ChatInputToolbar,
12
+ ChatSecondaryPanel,
13
+ cn,
14
+ HeightTransition,
15
+ Message,
16
+ MessageContent,
17
+ type TodoItem,
18
+ } from "@townco/ui/gui";
19
+ import { ChevronDown, SendIcon } from "lucide-react";
20
+ import { useEffect, useRef, useState } from "react";
21
+
22
+ export interface ChatViewProps {
23
+ client: AcpClient | null;
24
+ }
25
+
26
+ export function ChatView({ client }: ChatViewProps) {
27
+ // Use shared hooks from @townco/ui/core
28
+ const { connectionStatus, connect } = useChatSession(client);
29
+ const { messages } = useChatMessages(client);
30
+ const error = useChatStore((state) => state.error);
31
+
32
+ const messagesEndRef = useRef<HTMLDivElement>(null);
33
+ const [isHeaderExpanded, setIsHeaderExpanded] = useState(false);
34
+ const [todos] = useState<TodoItem[]>([]);
35
+
36
+ // Auto-scroll to bottom when new messages arrive
37
+ useEffect(() => {
38
+ messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
39
+ });
40
+
41
+ const getStatusColor = () => {
42
+ switch (connectionStatus) {
43
+ case "connected":
44
+ return "bg-green-500";
45
+ case "connecting":
46
+ return "bg-yellow-500";
47
+ case "error":
48
+ return "bg-red-500";
49
+ default:
50
+ return "bg-gray-500";
51
+ }
52
+ };
53
+
54
+ const getStatusText = () => {
55
+ switch (connectionStatus) {
56
+ case "connected":
57
+ return "Connected";
58
+ case "connecting":
59
+ return "Connecting...";
60
+ case "error":
61
+ return "Connection Error";
62
+ default:
63
+ return "No Server";
64
+ }
65
+ };
66
+
67
+ return (
68
+ <div className="flex flex-col h-screen bg-background text-foreground">
69
+ {/* Header */}
70
+ <div className="relative border-b border-border bg-card z-10">
71
+ <div className="flex items-center justify-between px-6 py-4">
72
+ <h1 className="text-xl font-semibold m-0">Agent Chat</h1>
73
+ <div className="flex items-center gap-3">
74
+ <div className="flex items-center gap-2">
75
+ <div className={cn("w-2 h-2 rounded-full", getStatusColor())} />
76
+ <span className="text-sm text-muted-foreground">
77
+ {getStatusText()}
78
+ </span>
79
+ </div>
80
+ <button
81
+ type="button"
82
+ onClick={() => setIsHeaderExpanded(!isHeaderExpanded)}
83
+ className="p-1 rounded hover:bg-background transition-colors"
84
+ aria-label={
85
+ isHeaderExpanded ? "Collapse header" : "Expand header"
86
+ }
87
+ >
88
+ <ChevronDown
89
+ className={cn(
90
+ "w-5 h-5 text-foreground transition-transform duration-200",
91
+ isHeaderExpanded && "rotate-180",
92
+ )}
93
+ />
94
+ </button>
95
+ </div>
96
+ </div>
97
+
98
+ <div className="absolute top-full left-0 right-0 z-20">
99
+ <HeightTransition>
100
+ {isHeaderExpanded && (
101
+ <div className="bg-card border-b border-border px-6 py-4 shadow-lg">
102
+ <ChatSecondaryPanel todos={todos} />
103
+ </div>
104
+ )}
105
+ </HeightTransition>
106
+ </div>
107
+ </div>
108
+
109
+ {/* Connection Error Banner */}
110
+ {connectionStatus === "error" && error && (
111
+ <div className="bg-red-500/10 border-b border-red-500/20 px-6 py-4">
112
+ <div className="flex items-start justify-between gap-4">
113
+ <div className="flex-1">
114
+ <h3 className="text-sm font-semibold text-red-500 mb-1">
115
+ Connection Error
116
+ </h3>
117
+ <p className="text-sm text-foreground whitespace-pre-line">
118
+ {error}
119
+ </p>
120
+ </div>
121
+ <button
122
+ type="button"
123
+ onClick={connect}
124
+ className="px-4 py-2 text-sm rounded-lg bg-red-500 text-white font-medium hover:bg-red-600 transition-colors"
125
+ >
126
+ Retry
127
+ </button>
128
+ </div>
129
+ </div>
130
+ )}
131
+
132
+ {/* Messages */}
133
+ <div className="flex-1 overflow-y-auto py-4">
134
+ {messages.length === 0 ? (
135
+ <div className="flex items-center justify-center h-full">
136
+ <div className="text-center text-muted-foreground">
137
+ <p className="text-lg mb-2">No messages yet</p>
138
+ <p className="text-sm">
139
+ {connectionStatus === "connected"
140
+ ? "Start a conversation with the agent"
141
+ : "Type a message to test the UI (no server connected)"}
142
+ </p>
143
+ </div>
144
+ </div>
145
+ ) : (
146
+ <div className="flex flex-col gap-4 px-4">
147
+ {messages.map((message) => (
148
+ <Message key={message.id} message={message}>
149
+ <MessageContent
150
+ message={message}
151
+ thinkingDisplayStyle="collapsible"
152
+ />
153
+ </Message>
154
+ ))}
155
+ </div>
156
+ )}
157
+ <div ref={messagesEndRef} />
158
+ </div>
159
+
160
+ {/* Input area */}
161
+ <div className="p-4 bg-background">
162
+ <ChatInputRoot client={client}>
163
+ <ChatInputField placeholder="Type a message..." />
164
+ <ChatInputToolbar className="justify-end">
165
+ <ChatInputSubmit>
166
+ <SendIcon className="size-4" />
167
+ </ChatInputSubmit>
168
+ </ChatInputToolbar>
169
+ </ChatInputRoot>
170
+ </div>
171
+ </div>
172
+ );
173
+ }
package/src/config.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Get the agent server URL from environment variables
3
+ * Falls back to localhost:3000 if not set
4
+ */
5
+ export function getAgentServerUrl(): string {
6
+ return import.meta.env.VITE_AGENT_URL || "http://localhost:3000";
7
+ }
8
+
9
+ /**
10
+ * Application configuration
11
+ */
12
+ export const config = {
13
+ agentServerUrl: getAgentServerUrl(),
14
+ } as const;
package/src/env.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_AGENT_URL?: string;
5
+ }
6
+
7
+ interface ImportMeta {
8
+ readonly env: ImportMetaEnv;
9
+ }
package/src/main.tsx ADDED
@@ -0,0 +1,13 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import App from "./App.js";
4
+ import "@townco/ui/styles/global.css";
5
+
6
+ const rootElement = document.getElementById("root");
7
+ if (!rootElement) throw new Error("Root element not found");
8
+
9
+ ReactDOM.createRoot(rootElement).render(
10
+ <React.StrictMode>
11
+ <App />
12
+ </React.StrictMode>,
13
+ );
package/vite.config.ts ADDED
@@ -0,0 +1,94 @@
1
+ import path from "node:path";
2
+ import react from "@vitejs/plugin-react";
3
+ import { defineConfig } from "vite";
4
+
5
+ // Create a stub module for stdio transport to prevent Node.js dependencies
6
+ const stdioStubPlugin = () => ({
7
+ name: "stdio-stub",
8
+ resolveId(id: string) {
9
+ // Intercept stdio transport imports and replace with stub
10
+ if (
11
+ id.includes("sdk/transports/stdio") ||
12
+ id.includes("transports/stdio.js")
13
+ ) {
14
+ return id;
15
+ }
16
+ return null;
17
+ },
18
+ load(id: string) {
19
+ if (
20
+ id.includes("sdk/transports/stdio") ||
21
+ id.includes("transports/stdio.js")
22
+ ) {
23
+ // Return a stub implementation that throws an error if used
24
+ return `
25
+ export class StdioTransport {
26
+ constructor() {
27
+ throw new Error("StdioTransport is not available in the browser. Use HttpTransport or WebSocketTransport instead.");
28
+ }
29
+ async connect() { throw new Error("StdioTransport not available in browser"); }
30
+ async disconnect() {}
31
+ async send() { throw new Error("StdioTransport not available in browser"); }
32
+ async *receive() { throw new Error("StdioTransport not available in browser"); }
33
+ isConnected() { return false; }
34
+ onSessionUpdate() { return () => {}; }
35
+ onError() { return () => {}; }
36
+ }
37
+ export const StdioTransportOptions = {};
38
+ `;
39
+ }
40
+ return null;
41
+ },
42
+ });
43
+
44
+ // https://vitejs.dev/config/
45
+ export default defineConfig({
46
+ plugins: [react(), stdioStubPlugin()],
47
+ resolve: {
48
+ alias: {
49
+ "@": path.resolve(__dirname, "./src"),
50
+ },
51
+ },
52
+ optimizeDeps: {
53
+ exclude: [],
54
+ },
55
+ build: {
56
+ rollupOptions: {
57
+ output: {
58
+ manualChunks: (id) => {
59
+ // Split React and ReactDOM into their own chunk
60
+ if (
61
+ id.includes("node_modules/react") ||
62
+ id.includes("node_modules/react-dom")
63
+ ) {
64
+ return "react";
65
+ }
66
+ // Split Radix UI components into their own chunk
67
+ if (id.includes("node_modules/@radix-ui")) {
68
+ return "radix";
69
+ }
70
+ // Split markdown rendering into its own chunk
71
+ if (
72
+ id.includes("node_modules/react-markdown") ||
73
+ id.includes("node_modules/remark-gfm")
74
+ ) {
75
+ return "markdown";
76
+ }
77
+ // Split lucide icons into their own chunk
78
+ if (id.includes("node_modules/lucide-react")) {
79
+ return "icons";
80
+ }
81
+ // Split ACP SDK into its own chunk
82
+ if (id.includes("node_modules/@agentclientprotocol")) {
83
+ return "acp-sdk";
84
+ }
85
+ // Split other node_modules into a vendor chunk
86
+ if (id.includes("node_modules")) {
87
+ return "vendor";
88
+ }
89
+ },
90
+ },
91
+ },
92
+ chunkSizeWarningLimit: 600, // Slightly increase limit to avoid false positives
93
+ },
94
+ });