@castlekit/castle 0.0.1 → 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.
Files changed (63) hide show
  1. package/README.md +38 -1
  2. package/bin/castle.js +94 -0
  3. package/install.sh +722 -0
  4. package/next.config.ts +7 -0
  5. package/package.json +54 -5
  6. package/postcss.config.mjs +7 -0
  7. package/src/app/api/avatars/[id]/route.ts +75 -0
  8. package/src/app/api/openclaw/agents/route.ts +107 -0
  9. package/src/app/api/openclaw/config/route.ts +94 -0
  10. package/src/app/api/openclaw/events/route.ts +96 -0
  11. package/src/app/api/openclaw/logs/route.ts +59 -0
  12. package/src/app/api/openclaw/ping/route.ts +68 -0
  13. package/src/app/api/openclaw/restart/route.ts +65 -0
  14. package/src/app/api/openclaw/sessions/route.ts +62 -0
  15. package/src/app/globals.css +286 -0
  16. package/src/app/icon.png +0 -0
  17. package/src/app/layout.tsx +42 -0
  18. package/src/app/page.tsx +269 -0
  19. package/src/app/ui-kit/page.tsx +684 -0
  20. package/src/cli/onboarding.ts +576 -0
  21. package/src/components/dashboard/agent-status.tsx +107 -0
  22. package/src/components/dashboard/glass-card.tsx +28 -0
  23. package/src/components/dashboard/goal-widget.tsx +174 -0
  24. package/src/components/dashboard/greeting-widget.tsx +78 -0
  25. package/src/components/dashboard/index.ts +7 -0
  26. package/src/components/dashboard/stat-widget.tsx +61 -0
  27. package/src/components/dashboard/stock-widget.tsx +164 -0
  28. package/src/components/dashboard/weather-widget.tsx +68 -0
  29. package/src/components/icons/castle-icon.tsx +21 -0
  30. package/src/components/kanban/index.ts +3 -0
  31. package/src/components/kanban/kanban-board.tsx +391 -0
  32. package/src/components/kanban/kanban-card.tsx +137 -0
  33. package/src/components/kanban/kanban-column.tsx +98 -0
  34. package/src/components/layout/index.ts +4 -0
  35. package/src/components/layout/page-header.tsx +20 -0
  36. package/src/components/layout/sidebar.tsx +128 -0
  37. package/src/components/layout/theme-toggle.tsx +59 -0
  38. package/src/components/layout/user-menu.tsx +72 -0
  39. package/src/components/ui/alert.tsx +72 -0
  40. package/src/components/ui/avatar.tsx +87 -0
  41. package/src/components/ui/badge.tsx +39 -0
  42. package/src/components/ui/button.tsx +43 -0
  43. package/src/components/ui/card.tsx +107 -0
  44. package/src/components/ui/checkbox.tsx +56 -0
  45. package/src/components/ui/clock.tsx +171 -0
  46. package/src/components/ui/dialog.tsx +105 -0
  47. package/src/components/ui/index.ts +34 -0
  48. package/src/components/ui/input.tsx +112 -0
  49. package/src/components/ui/option-card.tsx +151 -0
  50. package/src/components/ui/progress.tsx +103 -0
  51. package/src/components/ui/radio.tsx +109 -0
  52. package/src/components/ui/select.tsx +46 -0
  53. package/src/components/ui/slider.tsx +62 -0
  54. package/src/components/ui/tabs.tsx +132 -0
  55. package/src/components/ui/toggle-group.tsx +85 -0
  56. package/src/components/ui/toggle.tsx +78 -0
  57. package/src/components/ui/tooltip.tsx +145 -0
  58. package/src/components/ui/uptime.tsx +106 -0
  59. package/src/lib/config.ts +195 -0
  60. package/src/lib/gateway-connection.ts +391 -0
  61. package/src/lib/hooks/use-openclaw.ts +163 -0
  62. package/src/lib/utils.ts +6 -0
  63. package/tsconfig.json +34 -0
@@ -0,0 +1,62 @@
1
+ import { NextResponse } from "next/server";
2
+ import { readdirSync, readFileSync, existsSync, statSync } from "fs";
3
+ import { join, basename } from "path";
4
+ import { getOpenClawDir } from "@/lib/config";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ /**
9
+ * GET /api/openclaw/sessions
10
+ * God mode: reads session files directly from ~/.openclaw/ filesystem.
11
+ * Returns a list of sessions with basic metadata.
12
+ */
13
+ export async function GET() {
14
+ const openclawDir = getOpenClawDir();
15
+ const agentsDir = join(openclawDir, "agents");
16
+
17
+ if (!existsSync(agentsDir)) {
18
+ return NextResponse.json({ sessions: [] });
19
+ }
20
+
21
+ try {
22
+ const sessions: Array<{
23
+ agentId: string;
24
+ sessionId: string;
25
+ sizeBytes: number;
26
+ modifiedAt: string;
27
+ }> = [];
28
+
29
+ // Scan all agent directories
30
+ const agentDirs = readdirSync(agentsDir, { withFileTypes: true })
31
+ .filter((d) => d.isDirectory());
32
+
33
+ for (const agentDir of agentDirs) {
34
+ const sessionsDir = join(agentsDir, agentDir.name, "sessions");
35
+ if (!existsSync(sessionsDir)) continue;
36
+
37
+ const files = readdirSync(sessionsDir)
38
+ .filter((f) => f.endsWith(".jsonl"));
39
+
40
+ for (const file of files) {
41
+ const filePath = join(sessionsDir, file);
42
+ const stat = statSync(filePath);
43
+ sessions.push({
44
+ agentId: agentDir.name,
45
+ sessionId: basename(file, ".jsonl"),
46
+ sizeBytes: stat.size,
47
+ modifiedAt: stat.mtime.toISOString(),
48
+ });
49
+ }
50
+ }
51
+
52
+ // Sort by most recently modified
53
+ sessions.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
54
+
55
+ return NextResponse.json({ sessions });
56
+ } catch (err) {
57
+ return NextResponse.json(
58
+ { error: err instanceof Error ? err.message : "Failed to read sessions" },
59
+ { status: 500 }
60
+ );
61
+ }
62
+ }
@@ -0,0 +1,286 @@
1
+ @import "tailwindcss";
2
+
3
+ /* ============================================
4
+ Castle Design System - Design Tokens
5
+ ============================================ */
6
+
7
+ :root {
8
+ /* Typography */
9
+ --font-sans: var(--font-geist-sans);
10
+ --font-mono: var(--font-geist-mono);
11
+
12
+ /* Spacing (8px base) */
13
+ --spacing-1: 4px;
14
+ --spacing-2: 8px;
15
+ --spacing-3: 12px;
16
+ --spacing-4: 16px;
17
+ --spacing-6: 24px;
18
+ --spacing-8: 32px;
19
+ --spacing-12: 48px;
20
+ --spacing-16: 64px;
21
+ --spacing-24: 96px;
22
+
23
+ /* Border Radius */
24
+ --radius-sm: 6px;
25
+ --radius-md: 12px;
26
+ --radius-lg: 16px;
27
+ --radius-full: 9999px;
28
+
29
+ /* Transitions */
30
+ --transition-fast: 150ms ease;
31
+ --transition-base: 200ms ease;
32
+ --transition-slow: 300ms ease;
33
+ }
34
+
35
+ /* Light Mode (default) */
36
+ :root {
37
+ --background: #f8fafc;
38
+ --background-secondary: #f1f5f9;
39
+ --surface: #ffffff;
40
+ --surface-hover: #f8fafc;
41
+ --border: #e2e8f0;
42
+ --border-hover: #cbd5e1;
43
+
44
+ --foreground: #0f172a;
45
+ --foreground-secondary: #475569;
46
+ --foreground-muted: #94a3b8;
47
+
48
+ --accent: #3b82f6;
49
+ --accent-hover: #2563eb;
50
+ --accent-foreground: #ffffff;
51
+
52
+ --success: #22c55e;
53
+ --warning: #f59e0b;
54
+ --error: #ef4444;
55
+ --info: #3b82f6;
56
+
57
+ /* Glass effect (light mode) */
58
+ --glass-bg: #ffffff7a;
59
+ --glass-border: #ffffff3d;
60
+ --glass-shadow: #0000000d;
61
+
62
+ /* Input (light mode) */
63
+ --input-background: #ffffff;
64
+ --input-border: #e2e8f0;
65
+ --input-focus: #3b82f6;
66
+
67
+ /* Logo */
68
+ --logo-color: #18181b;
69
+
70
+ /* Dashboard container */
71
+ --dashboard-bg: #f1f5f9;
72
+ --dashboard-overlay: transparent;
73
+ --greeting-text: #000000;
74
+ --greeting-text-secondary: rgba(0, 0, 0, 0.7);
75
+
76
+ /* Glass card background */
77
+ --glass-card-bg: rgba(255, 255, 255, 0.85);
78
+ }
79
+
80
+ /* Dark Mode */
81
+ .dark, :root.dark, html.dark {
82
+ --background: #1a1a1f;
83
+ --background-secondary: #141418;
84
+ --surface: #222228;
85
+ --surface-hover: #2a2a30;
86
+ --border: #35353d;
87
+ --border-hover: #45454d;
88
+
89
+ --foreground: #fafafa;
90
+ --foreground-secondary: #a1a1aa;
91
+ --foreground-muted: #71717a;
92
+
93
+ --accent: #3b82f6;
94
+ --accent-hover: #60a5fa;
95
+ --accent-foreground: #ffffff;
96
+
97
+ --success: #22c55e;
98
+ --warning: #f59e0b;
99
+ --error: #ef4444;
100
+ --info: #3b82f6;
101
+
102
+ /* Glass effect (dark mode) */
103
+ --glass-bg: rgba(30, 30, 35, 0.7);
104
+ --glass-border: rgba(255, 255, 255, 0.1);
105
+ --glass-shadow: rgba(0, 0, 0, 0.3);
106
+
107
+ /* Input (dark mode) */
108
+ --input-background: #141418;
109
+ --input-border: #35353d;
110
+ --input-focus: #3b82f6;
111
+
112
+ /* Logo */
113
+ --logo-color: #e8e8ec;
114
+
115
+ /* Dashboard container */
116
+ --dashboard-bg: #141418;
117
+ --dashboard-overlay: rgba(0, 0, 0, 0.5);
118
+ --greeting-text: #ffffff;
119
+ --greeting-text-secondary: rgba(255, 255, 255, 0.7);
120
+
121
+ /* Glass card background */
122
+ --glass-card-bg: rgba(255, 255, 255, 0.05);
123
+ }
124
+
125
+ @theme inline {
126
+ /* Colors */
127
+ --color-background: var(--background);
128
+ --color-background-secondary: var(--background-secondary);
129
+ --color-surface: var(--surface);
130
+ --color-surface-hover: var(--surface-hover);
131
+ --color-border: var(--border);
132
+ --color-border-hover: var(--border-hover);
133
+
134
+ --color-foreground: var(--foreground);
135
+ --color-foreground-secondary: var(--foreground-secondary);
136
+ --color-foreground-muted: var(--foreground-muted);
137
+
138
+ --color-accent: var(--accent);
139
+ --color-accent-hover: var(--accent-hover);
140
+ --color-accent-foreground: var(--accent-foreground);
141
+
142
+ --color-success: var(--success);
143
+ --color-warning: var(--warning);
144
+ --color-error: var(--error);
145
+ --color-info: var(--info);
146
+
147
+ /* Input specific */
148
+ --color-input-background: var(--input-background);
149
+ --color-input-border: var(--input-border);
150
+ --color-input-focus: var(--input-focus);
151
+
152
+ /* Typography */
153
+ --font-sans: var(--font-geist-sans);
154
+ --font-mono: var(--font-geist-mono);
155
+
156
+ /* Border Radius */
157
+ --radius-sm: var(--radius-sm);
158
+ --radius-md: var(--radius-md);
159
+ --radius-lg: var(--radius-lg);
160
+ --radius-full: var(--radius-full);
161
+ }
162
+
163
+ /* Base styles */
164
+ html {
165
+ /* Prevent viewport scrollbar "blink" during hydration/layout settling */
166
+ overflow-y: scroll;
167
+ scrollbar-gutter: stable;
168
+ overscroll-behavior: none;
169
+ }
170
+
171
+ /* Hint native controls/scrollbars about theme */
172
+ :root {
173
+ color-scheme: light;
174
+ }
175
+ .dark, :root.dark, html.dark {
176
+ color-scheme: dark;
177
+ }
178
+
179
+ body {
180
+ background: var(--background);
181
+ color: var(--foreground);
182
+ font-family: var(--font-sans), system-ui, sans-serif;
183
+ -webkit-font-smoothing: antialiased;
184
+ -moz-osx-font-smoothing: grayscale;
185
+ /* Disable overscroll bounce effect */
186
+ overscroll-behavior: none;
187
+ }
188
+
189
+ /* ============================================
190
+ Reusable Component Patterns
191
+ ============================================ */
192
+
193
+ /* Glass effect - frosted glass style for dashboard/ambient UI */
194
+ .glass {
195
+ backdrop-filter: blur(3px);
196
+ border: 1px solid var(--glass-border);
197
+ background-color: var(--glass-bg);
198
+ box-shadow: 0 1px 2px var(--glass-shadow);
199
+ }
200
+
201
+ @layer utilities {
202
+ /* Focus ring - consistent focus state across interactive elements */
203
+ .focus-ring {
204
+ @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background;
205
+ }
206
+
207
+ /* Disabled state - consistent disabled styling */
208
+ .disabled-state {
209
+ @apply disabled:cursor-not-allowed disabled:opacity-50;
210
+ }
211
+
212
+ /* Label - consistent form label styling */
213
+ .form-label {
214
+ @apply block text-sm text-foreground-muted mb-2;
215
+ }
216
+ }
217
+
218
+ @layer components {
219
+ /* Interactive base - combines cursor, focus, and disabled */
220
+ .interactive {
221
+ @apply cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50;
222
+ }
223
+
224
+ /* Input base - shared input/select/textarea styles */
225
+ .input-base {
226
+ @apply h-11 w-full rounded-[var(--radius-sm)] border px-3 py-2 text-sm text-foreground transition-all;
227
+ @apply bg-[var(--input-background)] border-[var(--input-border)];
228
+ @apply focus:outline-none focus:border-[var(--input-focus)];
229
+ @apply disabled:cursor-not-allowed disabled:opacity-50;
230
+ }
231
+
232
+ .input-base[aria-invalid="true"],
233
+ .input-base.error {
234
+ @apply border-error focus:border-error;
235
+ }
236
+ }
237
+
238
+ /* Scrollbar styling (global) */
239
+ ::-webkit-scrollbar {
240
+ width: 8px;
241
+ height: 8px;
242
+ }
243
+
244
+ ::-webkit-scrollbar-track {
245
+ background: var(--background-secondary);
246
+ }
247
+
248
+ ::-webkit-scrollbar-thumb {
249
+ background: var(--border);
250
+ border-radius: var(--radius-full);
251
+ }
252
+
253
+ ::-webkit-scrollbar-thumb:hover {
254
+ background: var(--border-hover);
255
+ }
256
+
257
+ /* Heart glow animation */
258
+ @keyframes heart-glow {
259
+ 0% {
260
+ transform: scale(1);
261
+ opacity: 0.6;
262
+ }
263
+ 100% {
264
+ transform: scale(2.5);
265
+ opacity: 0;
266
+ }
267
+ }
268
+
269
+ /* Heart beat pulse animation */
270
+ @keyframes heart-beat {
271
+ 0% {
272
+ transform: scale(1);
273
+ }
274
+ 25% {
275
+ transform: scale(1.3);
276
+ }
277
+ 50% {
278
+ transform: scale(1);
279
+ }
280
+ 75% {
281
+ transform: scale(1.15);
282
+ }
283
+ 100% {
284
+ transform: scale(1);
285
+ }
286
+ }
Binary file
@@ -0,0 +1,42 @@
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import { ThemeProvider } from "next-themes";
4
+ import "./globals.css";
5
+
6
+ const geistSans = Geist({
7
+ variable: "--font-geist-sans",
8
+ subsets: ["latin"],
9
+ });
10
+
11
+ const geistMono = Geist_Mono({
12
+ variable: "--font-geist-mono",
13
+ subsets: ["latin"],
14
+ });
15
+
16
+ export const metadata: Metadata = {
17
+ title: "Castle",
18
+ description: "The multi-agent workspace",
19
+ };
20
+
21
+ export default function RootLayout({
22
+ children,
23
+ }: Readonly<{
24
+ children: React.ReactNode;
25
+ }>) {
26
+ return (
27
+ <html lang="en" suppressHydrationWarning>
28
+ <body
29
+ className={`${geistSans.variable} ${geistMono.variable} antialiased bg-background text-foreground`}
30
+ >
31
+ <ThemeProvider
32
+ attribute="class"
33
+ defaultTheme="dark"
34
+ enableSystem={false}
35
+ disableTransitionOnChange
36
+ >
37
+ {children}
38
+ </ThemeProvider>
39
+ </body>
40
+ </html>
41
+ );
42
+ }
@@ -0,0 +1,269 @@
1
+ "use client";
2
+
3
+ import { Bot, Wifi, WifiOff, Crown, RefreshCw, Loader2, AlertCircle } from "lucide-react";
4
+ import { Sidebar } from "@/components/layout/sidebar";
5
+ import { UserMenu } from "@/components/layout/user-menu";
6
+ import { PageHeader } from "@/components/layout/page-header";
7
+ import { Card, CardContent } from "@/components/ui/card";
8
+ import { Badge } from "@/components/ui/badge";
9
+ import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
10
+ import { cn } from "@/lib/utils";
11
+ import { useOpenClaw, type OpenClawAgent } from "@/lib/hooks/use-openclaw";
12
+
13
+ function getInitials(name: string) {
14
+ return name.slice(0, 2).toUpperCase();
15
+ }
16
+
17
+ function AgentCard({ agent, isPrimary, isConnected }: { agent: OpenClawAgent; isPrimary: boolean; isConnected: boolean }) {
18
+ return (
19
+ <Card
20
+ variant="bordered"
21
+ className={cn(
22
+ "p-4 transition-colors",
23
+ isConnected
24
+ ? "hover:border-border-hover"
25
+ : "opacity-60"
26
+ )}
27
+ >
28
+ <div className="flex items-center justify-between">
29
+ <div className="flex items-center gap-3">
30
+ <Avatar size="md" status={isConnected ? "online" : "offline"}>
31
+ {agent.avatar ? (
32
+ <AvatarImage
33
+ src={agent.avatar}
34
+ alt={agent.name}
35
+ className={cn(!isConnected && "grayscale")}
36
+ />
37
+ ) : (
38
+ <AvatarFallback>
39
+ {agent.emoji || getInitials(agent.name)}
40
+ </AvatarFallback>
41
+ )}
42
+ </Avatar>
43
+ <div>
44
+ <div className="flex items-center gap-2">
45
+ <p className="text-sm font-medium text-foreground">
46
+ {agent.name}
47
+ </p>
48
+ {isPrimary && (
49
+ <span className="flex items-center gap-1 text-xs text-accent">
50
+ <Crown className="h-3 w-3" />
51
+ Primary
52
+ </span>
53
+ )}
54
+ </div>
55
+ <p className="text-xs text-foreground-muted">
56
+ {isConnected
57
+ ? (agent.description || "OpenClaw Agent")
58
+ : "Unreachable"}
59
+ </p>
60
+ </div>
61
+ </div>
62
+ <Badge
63
+ variant={isConnected ? "success" : "outline"}
64
+ size="sm"
65
+ >
66
+ {isConnected ? "Active" : "Offline"}
67
+ </Badge>
68
+ </div>
69
+ </Card>
70
+ );
71
+ }
72
+
73
+ function ConnectionCard({
74
+ isConnected,
75
+ isLoading,
76
+ isConfigured,
77
+ serverVersion,
78
+ latency,
79
+ error,
80
+ onRefresh,
81
+ }: {
82
+ isConnected: boolean;
83
+ isLoading: boolean;
84
+ isConfigured: boolean;
85
+ serverVersion?: string;
86
+ latency?: number;
87
+ error?: string;
88
+ onRefresh: () => void;
89
+ }) {
90
+ const getSubtitle = () => {
91
+ if (isLoading) return "Connecting to Gateway...";
92
+ if (!isConfigured) return "Run 'castle setup' to configure";
93
+ if (isConnected) {
94
+ const parts = ["Connected"];
95
+ if (serverVersion) parts[0] = `Connected to OpenClaw v${serverVersion}`;
96
+ if (latency) parts.push(`${latency}ms`);
97
+ return parts.join(" · ");
98
+ }
99
+ return error || "Not connected";
100
+ };
101
+
102
+ return (
103
+ <Card variant="bordered" className="mb-8">
104
+ <CardContent className="flex items-center justify-between">
105
+ <div className="flex items-center gap-3">
106
+ {isLoading ? (
107
+ <Loader2 className="h-5 w-5 text-foreground-muted animate-spin" />
108
+ ) : isConnected ? (
109
+ <Wifi className="h-5 w-5 text-success" />
110
+ ) : (
111
+ <WifiOff className="h-5 w-5 text-error" />
112
+ )}
113
+ <div>
114
+ <p className="text-sm font-medium text-foreground">
115
+ OpenClaw Gateway
116
+ </p>
117
+ <p className="text-xs text-foreground-muted">
118
+ {getSubtitle()}
119
+ </p>
120
+ </div>
121
+ </div>
122
+ <div className="flex items-center gap-2">
123
+ <button
124
+ onClick={onRefresh}
125
+ className="p-1.5 rounded-md text-foreground-muted hover:text-foreground hover:bg-surface-hover transition-colors"
126
+ title="Refresh connection"
127
+ >
128
+ <RefreshCw className="h-4 w-4" />
129
+ </button>
130
+ {isLoading ? (
131
+ <Badge variant="outline">Connecting...</Badge>
132
+ ) : (
133
+ <Badge variant={isConnected ? "success" : "error"}>
134
+ {isConnected ? "Connected" : "Disconnected"}
135
+ </Badge>
136
+ )}
137
+ </div>
138
+ </CardContent>
139
+ </Card>
140
+ );
141
+ }
142
+
143
+ function AgentsSkeleton() {
144
+ return (
145
+ <div className="grid gap-3">
146
+ {[1, 2, 3].map((i) => (
147
+ <Card key={i} variant="bordered" className="p-4">
148
+ <div className="flex items-center gap-3">
149
+ <div className="h-10 w-10 rounded-full bg-surface-hover animate-pulse" />
150
+ <div className="space-y-2">
151
+ <div className="h-4 w-24 bg-surface-hover rounded animate-pulse" />
152
+ <div className="h-3 w-32 bg-surface-hover rounded animate-pulse" />
153
+ </div>
154
+ </div>
155
+ </Card>
156
+ ))}
157
+ </div>
158
+ );
159
+ }
160
+
161
+ function EmptyState({ isConfigured }: { isConfigured: boolean }) {
162
+ return (
163
+ <Card variant="bordered" className="p-8">
164
+ <div className="flex flex-col items-center text-center gap-3">
165
+ {isConfigured ? (
166
+ <>
167
+ <AlertCircle className="h-8 w-8 text-foreground-muted" />
168
+ <div>
169
+ <p className="text-sm font-medium text-foreground">
170
+ No agents discovered
171
+ </p>
172
+ <p className="text-xs text-foreground-muted mt-1">
173
+ Make sure OpenClaw Gateway is running and agents are configured.
174
+ </p>
175
+ </div>
176
+ </>
177
+ ) : (
178
+ <>
179
+ <Bot className="h-8 w-8 text-foreground-muted" />
180
+ <div>
181
+ <p className="text-sm font-medium text-foreground">
182
+ Welcome to Castle
183
+ </p>
184
+ <p className="text-xs text-foreground-muted mt-1">
185
+ Run <code className="px-1 py-0.5 bg-surface-hover rounded text-xs">castle setup</code> to connect to your OpenClaw Gateway.
186
+ </p>
187
+ </div>
188
+ </>
189
+ )}
190
+ </div>
191
+ </Card>
192
+ );
193
+ }
194
+
195
+ export default function HomePage() {
196
+ const {
197
+ status,
198
+ isLoading,
199
+ isConnected,
200
+ isConfigured,
201
+ latency,
202
+ serverVersion,
203
+ agents,
204
+ agentsLoading,
205
+ refresh,
206
+ } = useOpenClaw();
207
+
208
+ return (
209
+ <div className="min-h-screen bg-background">
210
+ <Sidebar variant="solid" />
211
+ <UserMenu className="fixed top-5 right-6 z-50" variant="solid" />
212
+
213
+ <main className="min-h-screen ml-[80px]">
214
+ <div className="p-8 max-w-4xl">
215
+ {/* Header */}
216
+ <div className="mb-8">
217
+ <PageHeader
218
+ title="Castle"
219
+ subtitle="The multi-agent workspace"
220
+ />
221
+ </div>
222
+
223
+ {/* OpenClaw Connection Status */}
224
+ <ConnectionCard
225
+ isConnected={isConnected}
226
+ isLoading={isLoading}
227
+ isConfigured={isConfigured}
228
+ serverVersion={serverVersion}
229
+ latency={latency}
230
+ error={status?.error}
231
+ onRefresh={refresh}
232
+ />
233
+
234
+ {/* Agents */}
235
+ <div className="space-y-4">
236
+ <div className="flex items-center justify-between">
237
+ <h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
238
+ <Bot className="h-5 w-5 text-foreground-secondary" />
239
+ Agents
240
+ </h2>
241
+ {!isLoading && agents.length > 0 && (
242
+ <span className="text-sm text-foreground-muted">
243
+ {agents.length} agent{agents.length !== 1 ? "s" : ""} discovered
244
+ </span>
245
+ )}
246
+ </div>
247
+
248
+ {agentsLoading && isLoading ? (
249
+ <AgentsSkeleton />
250
+ ) : agents.length > 0 ? (
251
+ <div className="grid gap-3">
252
+ {agents.map((agent, idx) => (
253
+ <AgentCard
254
+ key={agent.id}
255
+ agent={agent}
256
+ isPrimary={idx === 0}
257
+ isConnected={isConnected}
258
+ />
259
+ ))}
260
+ </div>
261
+ ) : (
262
+ <EmptyState isConfigured={isConfigured} />
263
+ )}
264
+ </div>
265
+ </div>
266
+ </main>
267
+ </div>
268
+ );
269
+ }