@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.
- package/README.md +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- 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-
|
|
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'}`} />
|