@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.
- package/README.md +38 -1
- package/bin/castle.js +94 -0
- package/install.sh +722 -0
- package/next.config.ts +7 -0
- package/package.json +54 -5
- package/postcss.config.mjs +7 -0
- package/src/app/api/avatars/[id]/route.ts +75 -0
- package/src/app/api/openclaw/agents/route.ts +107 -0
- package/src/app/api/openclaw/config/route.ts +94 -0
- package/src/app/api/openclaw/events/route.ts +96 -0
- package/src/app/api/openclaw/logs/route.ts +59 -0
- package/src/app/api/openclaw/ping/route.ts +68 -0
- package/src/app/api/openclaw/restart/route.ts +65 -0
- package/src/app/api/openclaw/sessions/route.ts +62 -0
- package/src/app/globals.css +286 -0
- package/src/app/icon.png +0 -0
- package/src/app/layout.tsx +42 -0
- package/src/app/page.tsx +269 -0
- package/src/app/ui-kit/page.tsx +684 -0
- package/src/cli/onboarding.ts +576 -0
- package/src/components/dashboard/agent-status.tsx +107 -0
- package/src/components/dashboard/glass-card.tsx +28 -0
- package/src/components/dashboard/goal-widget.tsx +174 -0
- package/src/components/dashboard/greeting-widget.tsx +78 -0
- package/src/components/dashboard/index.ts +7 -0
- package/src/components/dashboard/stat-widget.tsx +61 -0
- package/src/components/dashboard/stock-widget.tsx +164 -0
- package/src/components/dashboard/weather-widget.tsx +68 -0
- package/src/components/icons/castle-icon.tsx +21 -0
- package/src/components/kanban/index.ts +3 -0
- package/src/components/kanban/kanban-board.tsx +391 -0
- package/src/components/kanban/kanban-card.tsx +137 -0
- package/src/components/kanban/kanban-column.tsx +98 -0
- package/src/components/layout/index.ts +4 -0
- package/src/components/layout/page-header.tsx +20 -0
- package/src/components/layout/sidebar.tsx +128 -0
- package/src/components/layout/theme-toggle.tsx +59 -0
- package/src/components/layout/user-menu.tsx +72 -0
- package/src/components/ui/alert.tsx +72 -0
- package/src/components/ui/avatar.tsx +87 -0
- package/src/components/ui/badge.tsx +39 -0
- package/src/components/ui/button.tsx +43 -0
- package/src/components/ui/card.tsx +107 -0
- package/src/components/ui/checkbox.tsx +56 -0
- package/src/components/ui/clock.tsx +171 -0
- package/src/components/ui/dialog.tsx +105 -0
- package/src/components/ui/index.ts +34 -0
- package/src/components/ui/input.tsx +112 -0
- package/src/components/ui/option-card.tsx +151 -0
- package/src/components/ui/progress.tsx +103 -0
- package/src/components/ui/radio.tsx +109 -0
- package/src/components/ui/select.tsx +46 -0
- package/src/components/ui/slider.tsx +62 -0
- package/src/components/ui/tabs.tsx +132 -0
- package/src/components/ui/toggle-group.tsx +85 -0
- package/src/components/ui/toggle.tsx +78 -0
- package/src/components/ui/tooltip.tsx +145 -0
- package/src/components/ui/uptime.tsx +106 -0
- package/src/lib/config.ts +195 -0
- package/src/lib/gateway-connection.ts +391 -0
- package/src/lib/hooks/use-openclaw.ts +163 -0
- package/src/lib/utils.ts +6 -0
- 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
|
+
}
|
package/src/app/icon.png
ADDED
|
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
|
+
}
|
package/src/app/page.tsx
ADDED
|
@@ -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
|
+
}
|