@castlekit/castle 0.1.5 → 0.3.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 (68) hide show
  1. package/drizzle.config.ts +7 -0
  2. package/next.config.ts +1 -0
  3. package/package.json +25 -4
  4. package/src/app/api/avatars/[id]/route.ts +122 -25
  5. package/src/app/api/openclaw/agents/[id]/avatar/route.ts +216 -0
  6. package/src/app/api/openclaw/agents/route.ts +77 -41
  7. package/src/app/api/openclaw/agents/status/route.ts +55 -0
  8. package/src/app/api/openclaw/chat/attachments/route.ts +230 -0
  9. package/src/app/api/openclaw/chat/channels/route.ts +214 -0
  10. package/src/app/api/openclaw/chat/route.ts +272 -0
  11. package/src/app/api/openclaw/chat/search/route.ts +149 -0
  12. package/src/app/api/openclaw/chat/storage/route.ts +75 -0
  13. package/src/app/api/openclaw/config/route.ts +45 -4
  14. package/src/app/api/openclaw/events/route.ts +31 -2
  15. package/src/app/api/openclaw/logs/route.ts +20 -5
  16. package/src/app/api/openclaw/restart/route.ts +12 -4
  17. package/src/app/api/openclaw/session/status/route.ts +42 -0
  18. package/src/app/api/settings/avatar/route.ts +190 -0
  19. package/src/app/api/settings/route.ts +88 -0
  20. package/src/app/chat/[channelId]/error-boundary.tsx +64 -0
  21. package/src/app/chat/[channelId]/page.tsx +305 -0
  22. package/src/app/chat/layout.tsx +96 -0
  23. package/src/app/chat/page.tsx +52 -0
  24. package/src/app/globals.css +89 -2
  25. package/src/app/layout.tsx +7 -1
  26. package/src/app/page.tsx +147 -28
  27. package/src/app/settings/page.tsx +300 -0
  28. package/src/cli/onboarding.ts +202 -37
  29. package/src/components/chat/agent-mention-popup.tsx +89 -0
  30. package/src/components/chat/archived-channels.tsx +190 -0
  31. package/src/components/chat/channel-list.tsx +140 -0
  32. package/src/components/chat/chat-input.tsx +310 -0
  33. package/src/components/chat/create-channel-dialog.tsx +171 -0
  34. package/src/components/chat/markdown-content.tsx +205 -0
  35. package/src/components/chat/message-bubble.tsx +152 -0
  36. package/src/components/chat/message-list.tsx +508 -0
  37. package/src/components/chat/message-queue.tsx +68 -0
  38. package/src/components/chat/session-divider.tsx +61 -0
  39. package/src/components/chat/session-stats-panel.tsx +139 -0
  40. package/src/components/chat/storage-indicator.tsx +76 -0
  41. package/src/components/layout/sidebar.tsx +126 -45
  42. package/src/components/layout/user-menu.tsx +29 -4
  43. package/src/components/providers/presence-provider.tsx +8 -0
  44. package/src/components/providers/search-provider.tsx +81 -0
  45. package/src/components/search/search-dialog.tsx +269 -0
  46. package/src/components/ui/avatar.tsx +11 -9
  47. package/src/components/ui/dialog.tsx +10 -4
  48. package/src/components/ui/tooltip.tsx +25 -8
  49. package/src/components/ui/twemoji-text.tsx +37 -0
  50. package/src/lib/api-security.ts +188 -0
  51. package/src/lib/config.ts +36 -4
  52. package/src/lib/date-utils.ts +79 -0
  53. package/src/lib/db/__tests__/queries.test.ts +318 -0
  54. package/src/lib/db/index.ts +642 -0
  55. package/src/lib/db/queries.ts +1017 -0
  56. package/src/lib/db/schema.ts +160 -0
  57. package/src/lib/device-identity.ts +303 -0
  58. package/src/lib/gateway-connection.ts +273 -36
  59. package/src/lib/hooks/use-agent-status.ts +251 -0
  60. package/src/lib/hooks/use-chat.ts +775 -0
  61. package/src/lib/hooks/use-openclaw.ts +105 -70
  62. package/src/lib/hooks/use-search.ts +113 -0
  63. package/src/lib/hooks/use-session-stats.ts +57 -0
  64. package/src/lib/hooks/use-user-settings.ts +46 -0
  65. package/src/lib/types/chat.ts +186 -0
  66. package/src/lib/types/search.ts +60 -0
  67. package/src/middleware.ts +52 -0
  68. package/vitest.config.ts +13 -0
@@ -0,0 +1,205 @@
1
+ "use client";
2
+
3
+ import React, { useState, useCallback } from "react";
4
+ import ReactMarkdown from "react-markdown";
5
+ import remarkGfm from "remark-gfm";
6
+ import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
7
+ import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
8
+ import { Copy, Check } from "lucide-react";
9
+ import { cn } from "@/lib/utils";
10
+
11
+ interface MarkdownContentProps {
12
+ content: string;
13
+ className?: string;
14
+ }
15
+
16
+ function CopyButton({ text }: { text: string }) {
17
+ const [copied, setCopied] = useState(false);
18
+
19
+ const handleCopy = useCallback(async () => {
20
+ try {
21
+ await navigator.clipboard.writeText(text);
22
+ setCopied(true);
23
+ setTimeout(() => setCopied(false), 2000);
24
+ } catch {
25
+ // Fallback for non-HTTPS
26
+ const textarea = document.createElement("textarea");
27
+ textarea.value = text;
28
+ document.body.appendChild(textarea);
29
+ textarea.select();
30
+ document.execCommand("copy");
31
+ document.body.removeChild(textarea);
32
+ setCopied(true);
33
+ setTimeout(() => setCopied(false), 2000);
34
+ }
35
+ }, [text]);
36
+
37
+ return (
38
+ <button
39
+ type="button"
40
+ onClick={handleCopy}
41
+ className="absolute top-2 right-2 p-1.5 rounded-md bg-white/10 hover:bg-white/20 transition-colors text-white/60 hover:text-white/90"
42
+ title={copied ? "Copied!" : "Copy code"}
43
+ >
44
+ {copied ? <Check className="h-3.5 w-3.5" /> : <Copy className="h-3.5 w-3.5" />}
45
+ </button>
46
+ );
47
+ }
48
+
49
+ /**
50
+ * Renders markdown content with syntax highlighting for code blocks.
51
+ * HTML rendering is disabled for XSS safety.
52
+ */
53
+ export function MarkdownContent({ content, className }: MarkdownContentProps) {
54
+ return (
55
+ <div className={cn("prose prose-sm dark:prose-invert max-w-none [&>p]:my-0", className)}>
56
+ <ReactMarkdown
57
+ remarkPlugins={[remarkGfm]}
58
+ components={{
59
+ // Code blocks with syntax highlighting
60
+ code({ className: codeClassName, children, ...props }) {
61
+ const match = /language-(\w+)/.exec(codeClassName || "");
62
+ const codeString = String(children).replace(/\n$/, "");
63
+
64
+ // Block code (with language tag)
65
+ if (match) {
66
+ return (
67
+ <div className="relative group not-prose my-3">
68
+ <div className="flex items-center justify-between px-4 py-1.5 bg-[#1e1e2e] rounded-t-lg border-b border-white/10">
69
+ <span className="text-xs text-white/40 font-mono">{match[1]}</span>
70
+ <CopyButton text={codeString} />
71
+ </div>
72
+ <SyntaxHighlighter
73
+ style={oneDark}
74
+ language={match[1]}
75
+ PreTag="div"
76
+ customStyle={{
77
+ margin: 0,
78
+ borderTopLeftRadius: 0,
79
+ borderTopRightRadius: 0,
80
+ borderBottomLeftRadius: "0.5rem",
81
+ borderBottomRightRadius: "0.5rem",
82
+ fontSize: "0.8125rem",
83
+ lineHeight: "1.5",
84
+ }}
85
+ >
86
+ {codeString}
87
+ </SyntaxHighlighter>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ // Multi-line code without language
93
+ if (codeString.includes("\n")) {
94
+ return (
95
+ <div className="relative group not-prose my-3">
96
+ <CopyButton text={codeString} />
97
+ <SyntaxHighlighter
98
+ style={oneDark}
99
+ PreTag="div"
100
+ customStyle={{
101
+ borderRadius: "0.5rem",
102
+ fontSize: "0.8125rem",
103
+ lineHeight: "1.5",
104
+ }}
105
+ >
106
+ {codeString}
107
+ </SyntaxHighlighter>
108
+ </div>
109
+ );
110
+ }
111
+
112
+ // Inline code
113
+ return (
114
+ <code
115
+ className="px-1.5 py-0.5 rounded bg-surface-hover font-mono text-[0.8125rem]"
116
+ {...props}
117
+ >
118
+ {children}
119
+ </code>
120
+ );
121
+ },
122
+
123
+ // Links
124
+ a({ href, children }) {
125
+ return (
126
+ <a
127
+ href={href}
128
+ target="_blank"
129
+ rel="noopener noreferrer"
130
+ className="text-accent hover:underline"
131
+ >
132
+ {children}
133
+ </a>
134
+ );
135
+ },
136
+
137
+ // Tables
138
+ table({ children }) {
139
+ return (
140
+ <div className="overflow-x-auto my-3 not-prose">
141
+ <table className="min-w-full text-sm border-collapse">
142
+ {children}
143
+ </table>
144
+ </div>
145
+ );
146
+ },
147
+ th({ children }) {
148
+ return (
149
+ <th className="px-3 py-2 text-left text-xs font-medium text-foreground-secondary border-b border-border bg-surface-hover">
150
+ {children}
151
+ </th>
152
+ );
153
+ },
154
+ td({ children }) {
155
+ return (
156
+ <td className="px-3 py-2 text-sm border-b border-border/50">
157
+ {children}
158
+ </td>
159
+ );
160
+ },
161
+
162
+ // Blockquotes
163
+ blockquote({ children }) {
164
+ return (
165
+ <blockquote className="border-l-2 border-accent/50 pl-4 my-3 text-foreground-secondary italic">
166
+ {children}
167
+ </blockquote>
168
+ );
169
+ },
170
+
171
+ // Lists
172
+ ul({ children }) {
173
+ return <ul className="list-disc pl-5 my-2 space-y-1">{children}</ul>;
174
+ },
175
+ ol({ children }) {
176
+ return <ol className="list-decimal pl-5 my-2 space-y-1">{children}</ol>;
177
+ },
178
+
179
+ // Horizontal rule
180
+ hr() {
181
+ return <hr className="my-4 border-border" />;
182
+ },
183
+
184
+ // Paragraphs
185
+ p({ children }) {
186
+ return <p>{children}</p>;
187
+ },
188
+
189
+ // Headings
190
+ h1({ children }) {
191
+ return <h1 className="text-lg font-bold mt-4 mb-2">{children}</h1>;
192
+ },
193
+ h2({ children }) {
194
+ return <h2 className="text-base font-bold mt-3 mb-1.5">{children}</h2>;
195
+ },
196
+ h3({ children }) {
197
+ return <h3 className="text-sm font-bold mt-2 mb-1">{children}</h3>;
198
+ },
199
+ }}
200
+ >
201
+ {content}
202
+ </ReactMarkdown>
203
+ </div>
204
+ );
205
+ }
@@ -0,0 +1,152 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Bot, User, AlertTriangle, StopCircle } from "lucide-react";
5
+ import { cn } from "@/lib/utils";
6
+ import { formatTime, formatDateTime } from "@/lib/date-utils";
7
+ import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
8
+ import { Tooltip } from "@/components/ui/tooltip";
9
+ import { MarkdownContent } from "./markdown-content";
10
+ import { TwemojiText } from "@/components/ui/twemoji-text";
11
+ import type { ChatMessage } from "@/lib/types/chat";
12
+ import type { AgentInfo } from "./agent-mention-popup";
13
+ import type { AgentStatus } from "@/lib/hooks/use-agent-status";
14
+
15
+ interface MessageBubbleProps {
16
+ message: ChatMessage;
17
+ isAgent: boolean;
18
+ agentName?: string;
19
+ agentAvatar?: string | null;
20
+ userAvatar?: string | null;
21
+ agents: AgentInfo[];
22
+ isStreaming?: boolean;
23
+ /** Whether to show avatar and name (false for consecutive messages from same sender) */
24
+ showHeader?: boolean;
25
+ agentStatus?: AgentStatus;
26
+ userStatus?: AgentStatus;
27
+ highlighted?: boolean;
28
+ }
29
+
30
+ export function MessageBubble({
31
+ message,
32
+ isAgent,
33
+ agentName,
34
+ agentAvatar,
35
+ userAvatar,
36
+ agents,
37
+ isStreaming,
38
+ showHeader = true,
39
+ agentStatus,
40
+ userStatus,
41
+ highlighted,
42
+ }: MessageBubbleProps) {
43
+ const formattedTime = formatTime(message.createdAt);
44
+ const fullDateTime = formatDateTime(message.createdAt);
45
+
46
+ // Prefer the name stored on the message (always available from DB, no FOUC).
47
+ // Fall back to the live agent name from the gateway, then a generic label.
48
+ const displayName = isAgent
49
+ ? (message.senderName || agentName || "Unknown Agent")
50
+ : "You";
51
+ const avatarSrc = isAgent ? agentAvatar : userAvatar;
52
+ const hasAttachments = message.attachments && message.attachments.length > 0;
53
+
54
+ // Map status to Avatar status prop
55
+ const avatarStatus = isAgent && agentStatus
56
+ ? ({ thinking: "away", active: "online", idle: "offline" } as const)[agentStatus]
57
+ : !isAgent && userStatus
58
+ ? ({ active: "online", idle: "offline", thinking: "away" } as const)[userStatus]
59
+ : undefined;
60
+
61
+ return (
62
+ <div className={cn("flex gap-3", showHeader ? "mb-[4px] first:mt-0" : "mt-0.5 pl-[48px]", highlighted && "bg-accent/10 -mx-2 px-2 py-1")}>
63
+ {/* Avatar — only shown on first message in a group */}
64
+ {showHeader && (
65
+ <div className="mt-0.5">
66
+ <Avatar size="sm" status={avatarStatus} statusPulse={agentStatus === "thinking"}>
67
+ {avatarSrc ? (
68
+ <AvatarImage src={avatarSrc} alt={displayName} />
69
+ ) : (
70
+ <AvatarFallback
71
+ className={cn(
72
+ isAgent ? "bg-accent/20 text-accent" : "bg-foreground/10 text-foreground"
73
+ )}
74
+ >
75
+ {isAgent ? <Bot className="w-4 h-4" /> : <User className="w-4 h-4" />}
76
+ </AvatarFallback>
77
+ )}
78
+ </Avatar>
79
+ </div>
80
+ )}
81
+
82
+ {/* Message content */}
83
+ <div className="flex flex-col min-w-0">
84
+ {/* Name + time header — only on first message in a group */}
85
+ {showHeader && (
86
+ <div className="flex items-baseline gap-2">
87
+ <span className="font-bold text-[15px] text-foreground">
88
+ {displayName}
89
+ </span>
90
+ <Tooltip content={fullDateTime} side="top" delay={800}>
91
+ <span className="text-xs text-foreground-secondary hover:text-foreground cursor-pointer transition-colors">
92
+ {formattedTime}
93
+ </span>
94
+ </Tooltip>
95
+ </div>
96
+ )}
97
+
98
+ {/* Attachments */}
99
+ {hasAttachments && (
100
+ <div className="flex gap-2 flex-wrap mb-1">
101
+ {message.attachments.map((att) => (
102
+ <img
103
+ key={att.id}
104
+ src={`/api/openclaw/chat/attachments?path=${encodeURIComponent(att.filePath)}`}
105
+ alt={att.originalName || "Attachment"}
106
+ className="max-w-[200px] max-h-[200px] rounded-lg object-cover border border-border"
107
+ />
108
+ ))}
109
+ </div>
110
+ )}
111
+
112
+ {/* Message text — no bubble, just plain text */}
113
+ <div className="text-[15px] text-foreground leading-[26px] break-words">
114
+ <TwemojiText>
115
+ {isAgent && message.content ? (
116
+ <MarkdownContent content={message.content} />
117
+ ) : (
118
+ <span className="whitespace-pre-wrap">{message.content}</span>
119
+ )}
120
+ </TwemojiText>
121
+
122
+ {/* Streaming cursor */}
123
+ {isStreaming && (
124
+ <span className="inline-block w-2 h-4 bg-foreground/50 animate-pulse ml-0.5 align-text-bottom" />
125
+ )}
126
+ </div>
127
+
128
+ {/* Status badges */}
129
+ {message.status === "interrupted" && (
130
+ <div className="flex items-center gap-1 text-xs text-warning mt-1">
131
+ <AlertTriangle className="h-3 w-3" />
132
+ <span>Response interrupted</span>
133
+ </div>
134
+ )}
135
+ {message.status === "aborted" && (
136
+ <div className="flex items-center gap-1 text-xs text-foreground-secondary mt-1">
137
+ <StopCircle className="h-3 w-3" />
138
+ <span>Response stopped</span>
139
+ </div>
140
+ )}
141
+
142
+ {/* Token info for agent messages */}
143
+ {isAgent && !isStreaming && (message.inputTokens || message.outputTokens) && (
144
+ <div className="text-xs text-foreground-secondary/60 flex items-center gap-2 mt-1">
145
+ {message.inputTokens != null && <span>{message.inputTokens.toLocaleString()} in</span>}
146
+ {message.outputTokens != null && <span>{message.outputTokens.toLocaleString()} out</span>}
147
+ </div>
148
+ )}
149
+ </div>
150
+ </div>
151
+ );
152
+ }