@swarmclawai/swarmclaw 0.5.2 → 0.6.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 (173) hide show
  1. package/README.md +42 -7
  2. package/bin/swarmclaw.js +76 -16
  3. package/next.config.ts +11 -1
  4. package/package.json +4 -2
  5. package/public/screenshots/agents.png +0 -0
  6. package/public/screenshots/dashboard.png +0 -0
  7. package/public/screenshots/providers.png +0 -0
  8. package/public/screenshots/tasks.png +0 -0
  9. package/scripts/postinstall.mjs +18 -0
  10. package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
  11. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  12. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  13. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  14. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  15. package/src/app/api/chatrooms/route.ts +50 -0
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/knowledge/[id]/route.ts +13 -2
  18. package/src/app/api/knowledge/route.ts +8 -1
  19. package/src/app/api/memory/route.ts +8 -0
  20. package/src/app/api/notifications/[id]/route.ts +27 -0
  21. package/src/app/api/notifications/route.ts +68 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +155 -0
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/route.ts +3 -3
  29. package/src/app/api/settings/route.ts +9 -0
  30. package/src/app/api/setup/check-provider/route.ts +3 -16
  31. package/src/app/api/skills/[id]/route.ts +6 -0
  32. package/src/app/api/skills/route.ts +6 -0
  33. package/src/app/api/tasks/[id]/route.ts +20 -0
  34. package/src/app/api/tasks/bulk/route.ts +100 -0
  35. package/src/app/api/tasks/route.ts +1 -0
  36. package/src/app/api/usage/route.ts +45 -0
  37. package/src/app/api/webhooks/[id]/route.ts +15 -1
  38. package/src/app/globals.css +58 -15
  39. package/src/app/page.tsx +142 -13
  40. package/src/cli/index.js +42 -0
  41. package/src/cli/index.test.js +30 -0
  42. package/src/cli/spec.js +32 -0
  43. package/src/components/agents/agent-avatar.tsx +57 -10
  44. package/src/components/agents/agent-card.tsx +48 -15
  45. package/src/components/agents/agent-chat-list.tsx +123 -10
  46. package/src/components/agents/agent-list.tsx +50 -19
  47. package/src/components/agents/agent-sheet.tsx +56 -63
  48. package/src/components/auth/access-key-gate.tsx +10 -3
  49. package/src/components/auth/setup-wizard.tsx +2 -2
  50. package/src/components/auth/user-picker.tsx +31 -3
  51. package/src/components/chat/activity-moment.tsx +169 -0
  52. package/src/components/chat/chat-header.tsx +2 -0
  53. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  54. package/src/components/chat/file-path-chip.tsx +125 -0
  55. package/src/components/chat/markdown-utils.ts +9 -0
  56. package/src/components/chat/message-bubble.tsx +46 -295
  57. package/src/components/chat/message-list.tsx +50 -1
  58. package/src/components/chat/streaming-bubble.tsx +36 -46
  59. package/src/components/chat/suggestions-bar.tsx +1 -1
  60. package/src/components/chat/thinking-indicator.tsx +72 -10
  61. package/src/components/chat/tool-call-bubble.tsx +66 -70
  62. package/src/components/chat/tool-request-banner.tsx +31 -7
  63. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  64. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  65. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  66. package/src/components/chatrooms/chatroom-list.tsx +123 -0
  67. package/src/components/chatrooms/chatroom-message.tsx +427 -0
  68. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  69. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  70. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  71. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  72. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  73. package/src/components/connectors/connector-sheet.tsx +34 -47
  74. package/src/components/home/home-view.tsx +501 -0
  75. package/src/components/input/chat-input.tsx +79 -41
  76. package/src/components/knowledge/knowledge-list.tsx +31 -1
  77. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  78. package/src/components/layout/app-layout.tsx +209 -83
  79. package/src/components/layout/mobile-header.tsx +2 -0
  80. package/src/components/layout/update-banner.tsx +2 -2
  81. package/src/components/logs/log-list.tsx +2 -2
  82. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  83. package/src/components/memory/memory-agent-list.tsx +143 -0
  84. package/src/components/memory/memory-browser.tsx +205 -0
  85. package/src/components/memory/memory-card.tsx +34 -7
  86. package/src/components/memory/memory-detail.tsx +359 -120
  87. package/src/components/memory/memory-sheet.tsx +157 -23
  88. package/src/components/plugins/plugin-list.tsx +1 -1
  89. package/src/components/plugins/plugin-sheet.tsx +1 -1
  90. package/src/components/projects/project-detail.tsx +509 -0
  91. package/src/components/projects/project-list.tsx +195 -59
  92. package/src/components/providers/provider-list.tsx +2 -2
  93. package/src/components/providers/provider-sheet.tsx +3 -3
  94. package/src/components/schedules/schedule-card.tsx +3 -2
  95. package/src/components/schedules/schedule-list.tsx +1 -1
  96. package/src/components/schedules/schedule-sheet.tsx +25 -25
  97. package/src/components/secrets/secret-sheet.tsx +47 -24
  98. package/src/components/secrets/secrets-list.tsx +18 -8
  99. package/src/components/sessions/new-session-sheet.tsx +33 -65
  100. package/src/components/sessions/session-card.tsx +45 -14
  101. package/src/components/sessions/session-list.tsx +35 -18
  102. package/src/components/shared/agent-picker-list.tsx +90 -0
  103. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  104. package/src/components/shared/attachment-chip.tsx +165 -0
  105. package/src/components/shared/avatar.tsx +10 -1
  106. package/src/components/shared/check-icon.tsx +12 -0
  107. package/src/components/shared/confirm-dialog.tsx +1 -1
  108. package/src/components/shared/empty-state.tsx +32 -0
  109. package/src/components/shared/file-preview.tsx +34 -0
  110. package/src/components/shared/form-styles.ts +2 -0
  111. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  112. package/src/components/shared/notification-center.tsx +223 -0
  113. package/src/components/shared/profile-sheet.tsx +115 -0
  114. package/src/components/shared/reply-quote.tsx +26 -0
  115. package/src/components/shared/search-dialog.tsx +296 -0
  116. package/src/components/shared/section-label.tsx +12 -0
  117. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  118. package/src/components/shared/settings/section-providers.tsx +1 -1
  119. package/src/components/shared/settings/section-secrets.tsx +1 -1
  120. package/src/components/shared/settings/section-theme.tsx +95 -0
  121. package/src/components/shared/settings/section-user-preferences.tsx +39 -0
  122. package/src/components/shared/settings/settings-page.tsx +180 -27
  123. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  124. package/src/components/shared/sheet-footer.tsx +33 -0
  125. package/src/components/skills/skill-list.tsx +61 -30
  126. package/src/components/skills/skill-sheet.tsx +81 -2
  127. package/src/components/tasks/task-board.tsx +448 -26
  128. package/src/components/tasks/task-card.tsx +46 -9
  129. package/src/components/tasks/task-column.tsx +62 -3
  130. package/src/components/tasks/task-list.tsx +12 -4
  131. package/src/components/tasks/task-sheet.tsx +89 -72
  132. package/src/components/ui/hover-card.tsx +52 -0
  133. package/src/components/usage/metrics-dashboard.tsx +78 -0
  134. package/src/components/usage/usage-list.tsx +1 -1
  135. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  136. package/src/hooks/use-view-router.ts +69 -19
  137. package/src/instrumentation.ts +15 -1
  138. package/src/lib/chat.ts +2 -0
  139. package/src/lib/cron-human.ts +114 -0
  140. package/src/lib/memory.ts +3 -0
  141. package/src/lib/server/chat-execution.ts +24 -4
  142. package/src/lib/server/connectors/manager.ts +11 -0
  143. package/src/lib/server/context-manager.ts +225 -13
  144. package/src/lib/server/create-notification.ts +42 -0
  145. package/src/lib/server/daemon-state.ts +165 -10
  146. package/src/lib/server/execution-log.ts +1 -0
  147. package/src/lib/server/heartbeat-service.ts +40 -5
  148. package/src/lib/server/heartbeat-wake.ts +110 -0
  149. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  150. package/src/lib/server/memory-consolidation.ts +92 -0
  151. package/src/lib/server/memory-db.ts +51 -6
  152. package/src/lib/server/openclaw-gateway.ts +9 -1
  153. package/src/lib/server/provider-health.ts +125 -0
  154. package/src/lib/server/queue.ts +5 -4
  155. package/src/lib/server/scheduler.ts +8 -0
  156. package/src/lib/server/session-run-manager.ts +4 -0
  157. package/src/lib/server/session-tools/chatroom.ts +136 -0
  158. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  159. package/src/lib/server/session-tools/index.ts +2 -0
  160. package/src/lib/server/session-tools/memory.ts +6 -1
  161. package/src/lib/server/storage.ts +80 -29
  162. package/src/lib/server/stream-agent-chat.ts +153 -47
  163. package/src/lib/server/system-events.ts +49 -0
  164. package/src/lib/server/ws-hub.ts +11 -0
  165. package/src/lib/soul-suggestions.ts +109 -0
  166. package/src/lib/tasks.ts +4 -1
  167. package/src/lib/view-routes.ts +36 -1
  168. package/src/lib/ws-client.ts +14 -4
  169. package/src/proxy.ts +79 -2
  170. package/src/stores/use-app-store.ts +94 -3
  171. package/src/stores/use-chat-store.ts +48 -3
  172. package/src/stores/use-chatroom-store.ts +276 -0
  173. package/src/types/index.ts +69 -2
@@ -0,0 +1,223 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState, useCallback } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { useWs } from '@/hooks/use-ws'
6
+ import type { AppNotification } from '@/types'
7
+
8
+ function timeAgo(ts: number): string {
9
+ const diff = Date.now() - ts
10
+ if (diff < 60_000) return 'just now'
11
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
12
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
13
+ return `${Math.floor(diff / 86_400_000)}d ago`
14
+ }
15
+
16
+ const TYPE_COLORS: Record<AppNotification['type'], string> = {
17
+ info: 'border-l-blue-400',
18
+ success: 'border-l-emerald-400',
19
+ warning: 'border-l-amber-400',
20
+ error: 'border-l-red-400',
21
+ }
22
+
23
+ const TYPE_ICONS: Record<AppNotification['type'], string> = {
24
+ info: 'i',
25
+ success: '\u2713',
26
+ warning: '!',
27
+ error: '\u2717',
28
+ }
29
+
30
+ const TYPE_ICON_COLORS: Record<AppNotification['type'], string> = {
31
+ info: 'text-blue-400',
32
+ success: 'text-emerald-400',
33
+ warning: 'text-amber-400',
34
+ error: 'text-red-400',
35
+ }
36
+
37
+ function resolveHttpUrl(raw: string | undefined): string | null {
38
+ if (!raw) return null
39
+ try {
40
+ const parsed = new URL(raw)
41
+ return parsed.protocol === 'http:' || parsed.protocol === 'https:' ? parsed.toString() : null
42
+ } catch {
43
+ return null
44
+ }
45
+ }
46
+
47
+ export function NotificationCenter({
48
+ variant = 'icon',
49
+ align = 'right',
50
+ direction = 'down',
51
+ }: {
52
+ variant?: 'icon' | 'row'
53
+ align?: 'left' | 'right'
54
+ direction?: 'up' | 'down'
55
+ }) {
56
+ const [open, setOpen] = useState(false)
57
+ const panelRef = useRef<HTMLDivElement>(null)
58
+ const buttonRef = useRef<HTMLButtonElement>(null)
59
+
60
+ const notifications = useAppStore((s) => s.notifications)
61
+ const unreadCount = useAppStore((s) => s.unreadNotificationCount)
62
+ const loadNotifications = useAppStore((s) => s.loadNotifications)
63
+ const markRead = useAppStore((s) => s.markNotificationRead)
64
+ const markAllRead = useAppStore((s) => s.markAllNotificationsRead)
65
+ const clearRead = useAppStore((s) => s.clearReadNotifications)
66
+
67
+ useEffect(() => {
68
+ loadNotifications()
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70
+ }, [])
71
+
72
+ const handleWsNotification = useCallback(() => {
73
+ loadNotifications()
74
+ // eslint-disable-next-line react-hooks/exhaustive-deps
75
+ }, [])
76
+ useWs('notifications', handleWsNotification, 30_000)
77
+
78
+ // Close panel when clicking outside
79
+ useEffect(() => {
80
+ if (!open) return
81
+ const handler = (e: MouseEvent) => {
82
+ if (
83
+ panelRef.current && !panelRef.current.contains(e.target as Node) &&
84
+ buttonRef.current && !buttonRef.current.contains(e.target as Node)
85
+ ) {
86
+ setOpen(false)
87
+ }
88
+ }
89
+ document.addEventListener('mousedown', handler)
90
+ return () => document.removeEventListener('mousedown', handler)
91
+ }, [open])
92
+
93
+ const handleNotificationClick = (n: AppNotification) => {
94
+ if (!n.read) {
95
+ markRead(n.id)
96
+ }
97
+ const actionUrl = resolveHttpUrl(n.actionUrl)
98
+ if (actionUrl) {
99
+ window.open(actionUrl, '_blank', 'noopener,noreferrer')
100
+ }
101
+ setOpen(false)
102
+ }
103
+
104
+ const isRow = variant === 'row'
105
+ const panelAlignClass = align === 'left' ? 'left-0' : 'right-0'
106
+ const panelDirectionClass = direction === 'up' ? 'bottom-full mb-2' : 'top-full mt-2'
107
+
108
+ return (
109
+ <div className="relative">
110
+ <button
111
+ ref={buttonRef}
112
+ onClick={() => setOpen((v) => !v)}
113
+ className={
114
+ isRow
115
+ ? 'relative w-full flex items-center gap-2.5 px-3 py-2 rounded-[10px] text-[13px] font-500 cursor-pointer transition-all bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04] border-none'
116
+ : 'relative flex items-center justify-center w-8 h-8 rounded-[8px] bg-transparent hover:bg-white/[0.05] transition-colors cursor-pointer border-none'
117
+ }
118
+ aria-label="Notifications"
119
+ title={unreadCount > 0 ? `${unreadCount} unread notifications` : 'Notifications'}
120
+ >
121
+ {/* Bell icon */}
122
+ <svg width={isRow ? '16' : '16'} height={isRow ? '16' : '16'} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={isRow ? 'text-text-3 shrink-0' : 'text-text-2'}>
123
+ <path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" />
124
+ <path d="M13.73 21a2 2 0 0 1-3.46 0" />
125
+ </svg>
126
+ {isRow && <span className="text-[13px] font-500">Notifications</span>}
127
+ {/* Badge */}
128
+ {unreadCount > 0 && (
129
+ <span className={isRow
130
+ ? 'ml-auto min-w-[18px] h-[18px] flex items-center justify-center rounded-full bg-red-500 text-[10px] font-700 text-white px-1 leading-none'
131
+ : 'absolute -top-0.5 -right-0.5 min-w-[16px] h-4 flex items-center justify-center rounded-full bg-red-500 text-[10px] font-700 text-white px-1 leading-none'}
132
+ >
133
+ {unreadCount > 99 ? '99+' : unreadCount}
134
+ </span>
135
+ )}
136
+ </button>
137
+
138
+ {open && (
139
+ <div
140
+ ref={panelRef}
141
+ className={`absolute ${panelAlignClass} ${panelDirectionClass} w-[340px] max-h-[460px] bg-raised border border-white/[0.06] rounded-[14px] shadow-[0_16px_64px_rgba(0,0,0,0.6)] backdrop-blur-xl z-90 flex flex-col overflow-hidden`}
142
+ style={{ animation: 'fade-in 0.15s cubic-bezier(0.16, 1, 0.3, 1)' }}
143
+ >
144
+ {/* Header */}
145
+ <div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.04] shrink-0">
146
+ <span className="text-[13px] font-600 text-text">Notifications</span>
147
+ <div className="flex items-center gap-2">
148
+ {unreadCount > 0 && (
149
+ <button
150
+ onClick={markAllRead}
151
+ className="text-[11px] font-500 text-text-3 hover:text-text cursor-pointer bg-transparent border-none transition-colors"
152
+ style={{ fontFamily: 'inherit' }}
153
+ >
154
+ Mark all read
155
+ </button>
156
+ )}
157
+ {notifications.some((n) => n.read) && (
158
+ <button
159
+ onClick={clearRead}
160
+ className="text-[11px] font-500 text-text-3 hover:text-text cursor-pointer bg-transparent border-none transition-colors"
161
+ style={{ fontFamily: 'inherit' }}
162
+ >
163
+ Clear read
164
+ </button>
165
+ )}
166
+ </div>
167
+ </div>
168
+
169
+ {/* List */}
170
+ <div className="flex-1 overflow-y-auto">
171
+ {notifications.length === 0 ? (
172
+ <div className="flex items-center justify-center py-10 text-[13px] text-text-3/50">
173
+ No notifications
174
+ </div>
175
+ ) : (
176
+ notifications.map((n) => (
177
+ <button
178
+ key={n.id}
179
+ onClick={() => handleNotificationClick(n)}
180
+ className={`w-full text-left px-4 py-3 border-l-[3px] border-b border-b-white/[0.03] bg-transparent
181
+ hover:bg-white/[0.03] transition-colors cursor-pointer border-t-0 border-r-0
182
+ ${TYPE_COLORS[n.type]}
183
+ ${n.read ? 'opacity-50' : ''}`}
184
+ style={{ fontFamily: 'inherit' }}
185
+ >
186
+ <div className="flex items-start gap-2.5">
187
+ <span className={`text-[12px] font-700 mt-0.5 shrink-0 w-4 text-center ${TYPE_ICON_COLORS[n.type]}`}>
188
+ {TYPE_ICONS[n.type]}
189
+ </span>
190
+ <div className="flex-1 min-w-0">
191
+ <div className="flex items-center gap-2">
192
+ <span className="text-[12px] font-600 text-text truncate flex-1">{n.title}</span>
193
+ <span className="text-[10px] text-text-3/50 shrink-0">{timeAgo(n.createdAt)}</span>
194
+ </div>
195
+ {n.message && (
196
+ <p className="text-[11px] text-text-3 mt-0.5 leading-relaxed line-clamp-2 m-0">
197
+ {n.message}
198
+ </p>
199
+ )}
200
+ {resolveHttpUrl(n.actionUrl) && (
201
+ <span className="inline-block mt-1 text-[11px] text-accent-bright/90">
202
+ {n.actionLabel || 'Open link'}
203
+ </span>
204
+ )}
205
+ {n.entityType && (
206
+ <span className="inline-block mt-1 text-[10px] text-text-3/40 font-mono">
207
+ {n.entityType}{n.entityId ? `:${n.entityId.slice(0, 8)}` : ''}
208
+ </span>
209
+ )}
210
+ </div>
211
+ {!n.read && (
212
+ <span className="w-2 h-2 rounded-full bg-blue-400 mt-1.5 shrink-0" />
213
+ )}
214
+ </div>
215
+ </button>
216
+ ))
217
+ )}
218
+ </div>
219
+ </div>
220
+ )}
221
+ </div>
222
+ )
223
+ }
@@ -0,0 +1,115 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
6
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
7
+ import { api } from '@/lib/api-client'
8
+
9
+ interface Props {
10
+ open: boolean
11
+ onClose: () => void
12
+ }
13
+
14
+ export function ProfileSheet({ open, onClose }: Props) {
15
+ const appSettings = useAppStore((s) => s.appSettings)
16
+ const loadSettings = useAppStore((s) => s.loadSettings)
17
+ const setUser = useAppStore((s) => s.setUser)
18
+ const currentUser = useAppStore((s) => s.currentUser)
19
+
20
+ const [name, setName] = useState('')
21
+ const [avatarSeed, setAvatarSeed] = useState('')
22
+ const [saving, setSaving] = useState(false)
23
+
24
+ useEffect(() => {
25
+ if (open) {
26
+ setName(appSettings.userName || currentUser || '')
27
+ setAvatarSeed(appSettings.userAvatarSeed || '')
28
+ }
29
+ }, [open, appSettings.userName, appSettings.userAvatarSeed, currentUser])
30
+
31
+ const handleSave = async () => {
32
+ const trimmed = name.trim()
33
+ if (!trimmed || saving) return
34
+ setSaving(true)
35
+ try {
36
+ await api('PUT', '/settings', {
37
+ userName: trimmed.toLowerCase(),
38
+ userAvatarSeed: avatarSeed.trim() || undefined,
39
+ })
40
+ setUser(trimmed.toLowerCase())
41
+ await loadSettings()
42
+ onClose()
43
+ } finally {
44
+ setSaving(false)
45
+ }
46
+ }
47
+
48
+ const handleSignOut = () => {
49
+ setUser(null)
50
+ onClose()
51
+ }
52
+
53
+ return (
54
+ <BottomSheet open={open} onClose={onClose}>
55
+ <div className="p-6 max-w-[400px] mx-auto">
56
+ <h2 className="font-display text-[18px] font-700 text-text mb-6 text-center">Profile</h2>
57
+
58
+ {/* Avatar preview */}
59
+ <div className="flex justify-center mb-6">
60
+ <AgentAvatar seed={avatarSeed || null} name={name || '?'} size={72} />
61
+ </div>
62
+
63
+ {/* Avatar seed */}
64
+ <div className="mb-4">
65
+ <label className="block text-[12px] font-600 text-text-2 mb-1.5">Avatar</label>
66
+ <div className="flex items-center gap-2">
67
+ <input
68
+ type="text"
69
+ value={avatarSeed}
70
+ onChange={(e) => setAvatarSeed(e.target.value)}
71
+ placeholder="Avatar seed (any text)"
72
+ className="flex-1 px-3 py-2 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[13px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40"
73
+ />
74
+ <button
75
+ type="button"
76
+ onClick={() => setAvatarSeed(Math.random().toString(36).slice(2, 10))}
77
+ className="px-3 py-2 rounded-[8px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600 cursor-pointer transition-all hover:bg-white/[0.04] shrink-0"
78
+ >
79
+ Randomize
80
+ </button>
81
+ </div>
82
+ </div>
83
+
84
+ {/* Name */}
85
+ <div className="mb-6">
86
+ <label className="block text-[12px] font-600 text-text-2 mb-1.5">Name</label>
87
+ <input
88
+ type="text"
89
+ value={name}
90
+ onChange={(e) => setName(e.target.value)}
91
+ placeholder="Your name"
92
+ className="w-full px-3 py-2 rounded-[8px] bg-white/[0.06] border border-white/[0.08] text-[13px] text-text placeholder:text-text-3 focus:outline-none focus:border-accent-bright/40"
93
+ />
94
+ </div>
95
+
96
+ {/* Save */}
97
+ <button
98
+ onClick={handleSave}
99
+ disabled={!name.trim() || saving}
100
+ className="w-full py-2.5 rounded-[8px] text-[13px] font-600 bg-accent-bright text-white hover:bg-accent-bright/90 transition-all disabled:opacity-50 cursor-pointer mb-4"
101
+ >
102
+ {saving ? 'Saving...' : 'Save'}
103
+ </button>
104
+
105
+ {/* Sign out */}
106
+ <button
107
+ onClick={handleSignOut}
108
+ className="w-full text-center text-[12px] text-text-3 hover:text-text-2 transition-all cursor-pointer bg-transparent border-none"
109
+ >
110
+ Sign in as different user
111
+ </button>
112
+ </div>
113
+ </BottomSheet>
114
+ )
115
+ }
@@ -0,0 +1,26 @@
1
+ 'use client'
2
+
3
+ interface Props {
4
+ senderName: string
5
+ text: string
6
+ onClick?: () => void
7
+ }
8
+
9
+ export function ReplyQuote({ senderName, text, onClick }: Props) {
10
+ const truncated = text.length > 120 ? text.slice(0, 120) + '...' : text
11
+ return (
12
+ <button
13
+ type="button"
14
+ onClick={onClick}
15
+ className="flex items-start gap-2 mb-1.5 text-left w-full bg-transparent border-none p-0 cursor-pointer group/reply"
16
+ >
17
+ <div className="w-0.5 shrink-0 self-stretch rounded-full bg-accent-bright/50" />
18
+ <div className="min-w-0 flex-1">
19
+ <span className="text-[11px] font-600 text-accent-bright">{senderName}</span>
20
+ <p className="text-[12px] text-text-3 leading-[1.4] break-words m-0 group-hover/reply:text-text-2 transition-colors">
21
+ {truncated}
22
+ </p>
23
+ </div>
24
+ </button>
25
+ )
26
+ }
@@ -0,0 +1,296 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useRef, useCallback } from 'react'
4
+ import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { api } from '@/lib/api-client'
7
+ import type { AppView } from '@/types'
8
+
9
+ interface SearchResult {
10
+ type: 'task' | 'agent' | 'session' | 'schedule' | 'webhook' | 'skill' | 'message'
11
+ id: string
12
+ title: string
13
+ description?: string
14
+ status?: string
15
+ messageIndex?: number
16
+ }
17
+
18
+ const TYPE_ICONS: Record<SearchResult['type'], string> = {
19
+ agent: 'M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2',
20
+ task: 'M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2',
21
+ session: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
22
+ schedule: 'M12 6v6l4 2',
23
+ webhook: 'M22 12h-4l-3 7L9 5l-3 7H2',
24
+ skill: 'M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z',
25
+ message: 'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
26
+ }
27
+
28
+ const TYPE_EXTRA_PATHS: Partial<Record<SearchResult['type'], string>> = {
29
+ agent: 'M12 7a4 4 0 1 0 0-0.01',
30
+ schedule: 'M12 12a10 10 0 1 0 0-0.01',
31
+ message: 'M8 10h8',
32
+ }
33
+
34
+ const TYPE_VIEW_MAP: Record<SearchResult['type'], AppView> = {
35
+ agent: 'agents',
36
+ task: 'tasks',
37
+ session: 'agents',
38
+ schedule: 'schedules',
39
+ webhook: 'webhooks',
40
+ skill: 'skills',
41
+ message: 'agents',
42
+ }
43
+
44
+ const TYPE_LABELS: Record<SearchResult['type'], string> = {
45
+ agent: 'Agent',
46
+ task: 'Task',
47
+ session: 'Chat',
48
+ schedule: 'Schedule',
49
+ webhook: 'Webhook',
50
+ skill: 'Skill',
51
+ message: 'Message',
52
+ }
53
+
54
+ export function SearchDialog() {
55
+ const [open, setOpen] = useState(false)
56
+ const [query, setQuery] = useState('')
57
+ const [results, setResults] = useState<SearchResult[]>([])
58
+ const [selectedIdx, setSelectedIdx] = useState(0)
59
+ const [loading, setLoading] = useState(false)
60
+ const inputRef = useRef<HTMLInputElement>(null)
61
+ const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
62
+ const listRef = useRef<HTMLDivElement>(null)
63
+
64
+ const setActiveView = useAppStore((s) => s.setActiveView)
65
+ const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
66
+ const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
67
+ const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
68
+ const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
69
+ const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
70
+ const setEditingScheduleId = useAppStore((s) => s.setEditingScheduleId)
71
+ const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
72
+ const setEditingWebhookId = useAppStore((s) => s.setEditingWebhookId)
73
+ const setWebhookSheetOpen = useAppStore((s) => s.setWebhookSheetOpen)
74
+ const setEditingSkillId = useAppStore((s) => s.setEditingSkillId)
75
+ const setSkillSheetOpen = useAppStore((s) => s.setSkillSheetOpen)
76
+ const setCurrentSession = useAppStore((s) => s.setCurrentSession)
77
+
78
+ // Global Cmd+K / Ctrl+K listener
79
+ useEffect(() => {
80
+ const handler = (e: KeyboardEvent) => {
81
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
82
+ e.preventDefault()
83
+ setOpen((v) => !v)
84
+ }
85
+ }
86
+ window.addEventListener('keydown', handler)
87
+ return () => window.removeEventListener('keydown', handler)
88
+ }, [])
89
+
90
+ // Listen for custom event from sidebar button
91
+ useEffect(() => {
92
+ const handler = () => setOpen(true)
93
+ window.addEventListener('swarmclaw:open-search', handler)
94
+ return () => window.removeEventListener('swarmclaw:open-search', handler)
95
+ }, [])
96
+
97
+ // Reset on open
98
+ useEffect(() => {
99
+ if (open) {
100
+ setQuery('')
101
+ setResults([])
102
+ setSelectedIdx(0)
103
+ setTimeout(() => inputRef.current?.focus(), 50)
104
+ }
105
+ }, [open])
106
+
107
+ // Debounced search
108
+ const doSearch = useCallback(async (q: string) => {
109
+ if (q.trim().length < 2) {
110
+ setResults([])
111
+ setLoading(false)
112
+ return
113
+ }
114
+ setLoading(true)
115
+ try {
116
+ const data = await api<{ results: SearchResult[] }>('GET', `/search?q=${encodeURIComponent(q)}`)
117
+ setResults(data.results)
118
+ setSelectedIdx(0)
119
+ } catch {
120
+ setResults([])
121
+ } finally {
122
+ setLoading(false)
123
+ }
124
+ }, [])
125
+
126
+ const handleQueryChange = (value: string) => {
127
+ setQuery(value)
128
+ if (debounceRef.current) clearTimeout(debounceRef.current)
129
+ debounceRef.current = setTimeout(() => doSearch(value), 300)
130
+ }
131
+
132
+ // Navigate to a result
133
+ const navigateTo = useCallback((result: SearchResult) => {
134
+ setOpen(false)
135
+ const view = TYPE_VIEW_MAP[result.type]
136
+ setActiveView(view)
137
+ setSidebarOpen(true)
138
+
139
+ switch (result.type) {
140
+ case 'agent':
141
+ setEditingAgentId(result.id)
142
+ setAgentSheetOpen(true)
143
+ break
144
+ case 'task':
145
+ setEditingTaskId(result.id)
146
+ setTaskSheetOpen(true)
147
+ break
148
+ case 'session':
149
+ setCurrentSession(result.id)
150
+ setActiveView('agents')
151
+ break
152
+ case 'message':
153
+ setCurrentSession(result.id)
154
+ setActiveView('agents')
155
+ break
156
+ case 'schedule':
157
+ setEditingScheduleId(result.id)
158
+ setScheduleSheetOpen(true)
159
+ break
160
+ case 'webhook':
161
+ setEditingWebhookId(result.id)
162
+ setWebhookSheetOpen(true)
163
+ break
164
+ case 'skill':
165
+ setEditingSkillId(result.id)
166
+ setSkillSheetOpen(true)
167
+ break
168
+ }
169
+ // eslint-disable-next-line react-hooks/exhaustive-deps
170
+ }, [])
171
+
172
+ // Keyboard navigation
173
+ const handleKeyDown = (e: React.KeyboardEvent) => {
174
+ if (e.key === 'ArrowDown') {
175
+ e.preventDefault()
176
+ setSelectedIdx((i) => Math.min(i + 1, results.length - 1))
177
+ } else if (e.key === 'ArrowUp') {
178
+ e.preventDefault()
179
+ setSelectedIdx((i) => Math.max(i - 1, 0))
180
+ } else if (e.key === 'Enter' && results[selectedIdx]) {
181
+ e.preventDefault()
182
+ navigateTo(results[selectedIdx])
183
+ }
184
+ }
185
+
186
+ // Scroll selected into view
187
+ useEffect(() => {
188
+ if (!listRef.current) return
189
+ const el = listRef.current.children[selectedIdx] as HTMLElement | undefined
190
+ el?.scrollIntoView({ block: 'nearest' })
191
+ }, [selectedIdx])
192
+
193
+ return (
194
+ <Dialog open={open} onOpenChange={setOpen}>
195
+ <DialogContent
196
+ showCloseButton={false}
197
+ className="sm:max-w-[520px] p-0 bg-[#1a1a2e]/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
198
+ onKeyDown={handleKeyDown}
199
+ >
200
+ <DialogTitle className="sr-only">Search</DialogTitle>
201
+ {/* Search input */}
202
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06]">
203
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3 shrink-0">
204
+ <circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
205
+ </svg>
206
+ <input
207
+ ref={inputRef}
208
+ value={query}
209
+ onChange={(e) => handleQueryChange(e.target.value)}
210
+ placeholder="Search agents, tasks, schedules..."
211
+ className="flex-1 bg-transparent border-none outline-none text-[14px] text-text placeholder:text-text-3/60 font-[inherit]"
212
+ autoFocus
213
+ />
214
+ <kbd className="px-1.5 py-0.5 rounded-[5px] bg-white/[0.06] border border-white/[0.08] text-[10px] font-mono text-text-3 shrink-0">
215
+ ESC
216
+ </kbd>
217
+ </div>
218
+
219
+ {/* Results */}
220
+ <div ref={listRef} className="max-h-[360px] overflow-y-auto py-1">
221
+ {loading && query.length >= 2 && (
222
+ <div className="px-4 py-8 text-center text-[13px] text-text-3">
223
+ Searching...
224
+ </div>
225
+ )}
226
+ {!loading && query.length >= 2 && results.length === 0 && (
227
+ <div className="px-4 py-8 text-center text-[13px] text-text-3">
228
+ No results found
229
+ </div>
230
+ )}
231
+ {!loading && query.length < 2 && (
232
+ <div className="px-4 py-8 text-center text-[13px] text-text-3/60">
233
+ Type at least 2 characters to search
234
+ </div>
235
+ )}
236
+ {results.map((result, idx) => (
237
+ <button
238
+ key={`${result.type}-${result.id}`}
239
+ onClick={() => navigateTo(result)}
240
+ onMouseEnter={() => setSelectedIdx(idx)}
241
+ className={`w-full flex items-center gap-3 px-4 py-2.5 text-left cursor-pointer transition-colors border-none bg-transparent
242
+ ${idx === selectedIdx ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`}
243
+ style={{ fontFamily: 'inherit' }}
244
+ >
245
+ {/* Type icon */}
246
+ <div className={`w-8 h-8 rounded-[8px] flex items-center justify-center shrink-0
247
+ ${idx === selectedIdx ? 'bg-accent-bright/20' : 'bg-white/[0.04]'}`}>
248
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"
249
+ className={idx === selectedIdx ? 'text-[#818CF8]' : 'text-text-3'}>
250
+ <path d={TYPE_ICONS[result.type]} />
251
+ {TYPE_EXTRA_PATHS[result.type] && <path d={TYPE_EXTRA_PATHS[result.type]} />}
252
+ </svg>
253
+ </div>
254
+ {/* Content */}
255
+ <div className="flex-1 min-w-0">
256
+ <div className="flex items-center gap-2">
257
+ <span className="text-[13px] font-500 text-text truncate">{result.title}</span>
258
+ {result.status && (
259
+ <span className="px-1.5 py-0.5 rounded-[4px] bg-white/[0.06] text-[10px] font-500 text-text-3 shrink-0">
260
+ {result.status}
261
+ </span>
262
+ )}
263
+ </div>
264
+ {result.description && (
265
+ <p className="text-[11px] text-text-3 truncate mt-0.5 m-0">{result.description}</p>
266
+ )}
267
+ </div>
268
+ {/* Type label */}
269
+ <span className="text-[10px] text-text-3/60 uppercase tracking-wider shrink-0">
270
+ {TYPE_LABELS[result.type]}
271
+ </span>
272
+ </button>
273
+ ))}
274
+ </div>
275
+
276
+ {/* Footer hint */}
277
+ {results.length > 0 && (
278
+ <div className="flex items-center gap-3 px-4 py-2 border-t border-white/[0.06] text-[11px] text-text-3/50">
279
+ <span className="flex items-center gap-1">
280
+ <kbd className="px-1 py-0.5 rounded bg-white/[0.06] text-[10px] font-mono">↑↓</kbd>
281
+ navigate
282
+ </span>
283
+ <span className="flex items-center gap-1">
284
+ <kbd className="px-1 py-0.5 rounded bg-white/[0.06] text-[10px] font-mono">↵</kbd>
285
+ open
286
+ </span>
287
+ <span className="flex items-center gap-1">
288
+ <kbd className="px-1 py-0.5 rounded bg-white/[0.06] text-[10px] font-mono">esc</kbd>
289
+ close
290
+ </span>
291
+ </div>
292
+ )}
293
+ </DialogContent>
294
+ </Dialog>
295
+ )
296
+ }
@@ -0,0 +1,12 @@
1
+ interface Props {
2
+ children: React.ReactNode
3
+ className?: string
4
+ }
5
+
6
+ export function SectionLabel({ children, className = '' }: Props) {
7
+ return (
8
+ <label className={`block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3 ${className}`}>
9
+ {children}
10
+ </label>
11
+ )
12
+ }
@@ -103,7 +103,7 @@ export function PluginManager() {
103
103
  <div
104
104
  onClick={() => togglePlugin(p.filename, !p.enabled)}
105
105
  className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer shrink-0
106
- ${p.enabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
106
+ ${p.enabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
107
107
  >
108
108
  <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
109
109
  ${p.enabled ? 'left-[22px]' : 'left-0.5'}`} />