@swarmclawai/swarmclaw 0.6.8 → 0.7.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 +70 -45
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +18 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +11 -3
- package/src/app/api/tasks/route.ts +8 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +13 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +86 -29
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +30 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +29 -6
- package/src/components/home/home-view.tsx +20 -14
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +73 -21
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +213 -59
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +19 -7
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +170 -66
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +66 -64
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +223 -62
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +42 -0
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +180 -17
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/orchestrator-lg.ts +4 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +650 -142
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/queue.ts +253 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +11 -1
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +85 -33
- package/src/lib/server/session-tools/index.ts +205 -160
- package/src/lib/server/session-tools/memory.ts +152 -265
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +66 -31
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +179 -349
- package/src/lib/server/storage.ts +24 -0
- package/src/lib/server/stream-agent-chat.ts +301 -244
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +23 -5
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +23 -23
- package/src/lib/validation/schemas.ts +12 -0
- package/src/lib/view-routes.ts +2 -24
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +121 -7
|
@@ -143,7 +143,7 @@ export function GatewayConnectionPanel() {
|
|
|
143
143
|
|
|
144
144
|
return (
|
|
145
145
|
<div className="flex flex-col gap-4 p-4">
|
|
146
|
-
<div className="flex items-center gap-2">
|
|
146
|
+
<div className="flex items-center gap-2" style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
|
|
147
147
|
<span className={`w-2 h-2 rounded-full ${dotColor}`} />
|
|
148
148
|
<span className="text-[13px] font-600 text-text capitalize">{status}</span>
|
|
149
149
|
{actionableIssues.length > 0 && (
|
|
@@ -153,7 +153,7 @@ export function GatewayConnectionPanel() {
|
|
|
153
153
|
)}
|
|
154
154
|
</div>
|
|
155
155
|
|
|
156
|
-
<div className="flex flex-col gap-2">
|
|
156
|
+
<div className="flex flex-col gap-2" style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.05s both' }}>
|
|
157
157
|
<label className="text-[11px] font-600 uppercase tracking-wider text-text-3/50">Gateway URL</label>
|
|
158
158
|
<input
|
|
159
159
|
type="text"
|
|
@@ -164,7 +164,7 @@ export function GatewayConnectionPanel() {
|
|
|
164
164
|
/>
|
|
165
165
|
</div>
|
|
166
166
|
|
|
167
|
-
<div className="flex flex-col gap-2">
|
|
167
|
+
<div className="flex flex-col gap-2" style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.1s both' }}>
|
|
168
168
|
<label className="text-[11px] font-600 uppercase tracking-wider text-text-3/50">Token (optional)</label>
|
|
169
169
|
<input
|
|
170
170
|
type="password"
|
|
@@ -175,7 +175,7 @@ export function GatewayConnectionPanel() {
|
|
|
175
175
|
/>
|
|
176
176
|
</div>
|
|
177
177
|
|
|
178
|
-
<div className="flex gap-2">
|
|
178
|
+
<div className="flex gap-2" style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.15s both' }}>
|
|
179
179
|
{status !== 'connected' ? (
|
|
180
180
|
<button
|
|
181
181
|
onClick={handleConnect}
|
|
@@ -204,12 +204,12 @@ export function GatewayConnectionPanel() {
|
|
|
204
204
|
</div>
|
|
205
205
|
|
|
206
206
|
{error && (
|
|
207
|
-
<p className="text-[12px] text-red-400">{error}</p>
|
|
207
|
+
<p className="text-[12px] text-red-400" style={{ animation: 'ai-shake 0.5s' }}>{error}</p>
|
|
208
208
|
)}
|
|
209
209
|
|
|
210
210
|
{/* Reload Mode Toggle (F21) */}
|
|
211
211
|
{status === 'connected' && (
|
|
212
|
-
<div className="flex flex-col gap-2 pt-2 border-t border-white/[0.04]">
|
|
212
|
+
<div className="flex flex-col gap-2 pt-2 border-t border-white/[0.04]" style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.2s both' }}>
|
|
213
213
|
<label className="text-[11px] font-600 uppercase tracking-wider text-text-3/50">Reload Mode</label>
|
|
214
214
|
<div className="flex gap-1">
|
|
215
215
|
{reloadModes.map((rm) => (
|
|
@@ -237,9 +237,9 @@ export function GatewayConnectionPanel() {
|
|
|
237
237
|
|
|
238
238
|
{/* Config Issues (F19) */}
|
|
239
239
|
{actionableIssues.length > 0 && (
|
|
240
|
-
<div className="flex flex-col gap-2 pt-2 border-t border-white/[0.04]">
|
|
240
|
+
<div className="flex flex-col gap-2 pt-2 border-t border-white/[0.04]" style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.25s both' }}>
|
|
241
241
|
<label className="text-[11px] font-600 uppercase tracking-wider text-text-3/50">Config Issues</label>
|
|
242
|
-
{actionableIssues.map((issue) => (
|
|
242
|
+
{actionableIssues.map((issue, idx) => (
|
|
243
243
|
<div
|
|
244
244
|
key={issue.id}
|
|
245
245
|
className={`flex items-start gap-3 p-3 rounded-[10px] border ${
|
|
@@ -247,6 +247,7 @@ export function GatewayConnectionPanel() {
|
|
|
247
247
|
? 'bg-red-400/[0.04] border-red-400/20'
|
|
248
248
|
: 'bg-amber-400/[0.04] border-amber-400/20'
|
|
249
249
|
}`}
|
|
250
|
+
style={{ animation: 'spring-in 0.4s var(--ease-spring) both', animationDelay: `${0.3 + idx * 0.05}s` }}
|
|
250
251
|
>
|
|
251
252
|
<span className={`mt-0.5 w-2 h-2 rounded-full shrink-0 ${
|
|
252
253
|
issue.severity === 'error' ? 'bg-red-400' : 'bg-amber-400'
|
|
@@ -172,11 +172,16 @@ export function CommandPalette() {
|
|
|
172
172
|
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={() => setOpen(false)} />
|
|
173
173
|
<div
|
|
174
174
|
className="relative w-full max-w-[520px] mx-4 bg-raised rounded-[16px] border border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] overflow-hidden"
|
|
175
|
-
style={{ animation: '
|
|
175
|
+
style={{ animation: 'modal-in 0.3s var(--ease-spring)' }}
|
|
176
176
|
>
|
|
177
|
+
{/* Breathing glow effect */}
|
|
178
|
+
<div className="absolute inset-0 pointer-events-none opacity-20" style={{ animation: 'glow-pulse 4s ease-in-out infinite' }}>
|
|
179
|
+
<div className="absolute inset-0 bg-gradient-to-b from-accent-bright/20 to-transparent" />
|
|
180
|
+
</div>
|
|
177
181
|
{/* Search input */}
|
|
178
|
-
<div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06]">
|
|
179
|
-
<
|
|
182
|
+
<div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] relative overflow-hidden">
|
|
183
|
+
<div className="absolute bottom-0 left-0 h-[1px] w-full bg-gradient-to-r from-transparent via-accent-bright/40 to-transparent" style={{ animation: 'glow-line 3s linear infinite' }} />
|
|
184
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3 shrink-0 relative z-10">
|
|
180
185
|
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
181
186
|
</svg>
|
|
182
187
|
<input
|
|
@@ -208,10 +213,13 @@ export function CommandPalette() {
|
|
|
208
213
|
<button
|
|
209
214
|
key={item.id}
|
|
210
215
|
onClick={item.onSelect}
|
|
211
|
-
className={`w-full flex items-center gap-3 px-4 py-2.5 text-left border-none cursor-pointer transition-
|
|
212
|
-
${idx === selectedIndex ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-2 hover:bg-white/[0.04]'}`}
|
|
216
|
+
className={`w-full flex items-center gap-3 px-4 py-2.5 text-left border-none cursor-pointer transition-all relative z-10
|
|
217
|
+
${idx === selectedIndex ? 'bg-accent-soft text-accent-bright pl-6 scale-[1.01]' : 'bg-transparent text-text-2 hover:bg-white/[0.04]'}`}
|
|
213
218
|
style={{ fontFamily: 'inherit' }}
|
|
214
219
|
>
|
|
220
|
+
{idx === selectedIndex && (
|
|
221
|
+
<div className="absolute left-0 top-1 bottom-1 w-1 rounded-r-full bg-accent-bright" style={{ animation: 'spring-in 0.3s var(--ease-spring)' }} />
|
|
222
|
+
)}
|
|
215
223
|
<span className="shrink-0 text-text-3">{categoryIcon[item.category as keyof typeof categoryIcon]}</span>
|
|
216
224
|
<span className="text-[13px] font-500 truncate">{item.label}</span>
|
|
217
225
|
</button>
|
|
@@ -10,21 +10,33 @@ interface Props {
|
|
|
10
10
|
|
|
11
11
|
export function EmptyState({ icon, title, subtitle, action }: Props) {
|
|
12
12
|
return (
|
|
13
|
-
<div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
|
|
14
|
-
<div
|
|
15
|
-
|
|
13
|
+
<div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center group/empty">
|
|
14
|
+
<div
|
|
15
|
+
className="w-14 h-14 rounded-[16px] bg-accent-soft flex items-center justify-center mb-1 relative"
|
|
16
|
+
style={{ animation: 'float 4s ease-in-out infinite' }}
|
|
17
|
+
>
|
|
18
|
+
<div className="text-accent-bright transition-transform duration-500 group-hover/empty:scale-110 group-hover/empty:rotate-[10deg]">
|
|
19
|
+
{icon}
|
|
20
|
+
</div>
|
|
21
|
+
{/* Subtle glow background */}
|
|
22
|
+
<div className="absolute inset-0 bg-accent-bright/5 blur-xl rounded-full opacity-0 group-hover/empty:opacity-100 transition-opacity" />
|
|
23
|
+
</div>
|
|
24
|
+
<div style={{ animation: 'fade-up 0.5s var(--ease-spring) both' }}>
|
|
25
|
+
<p className="font-display text-[15px] font-600 text-text-2">{title}</p>
|
|
26
|
+
{subtitle && <p className="text-[13px] text-text-3/50 mt-1">{subtitle}</p>}
|
|
16
27
|
</div>
|
|
17
|
-
<p className="font-display text-[15px] font-600 text-text-2">{title}</p>
|
|
18
|
-
{subtitle && <p className="text-[13px] text-text-3/50">{subtitle}</p>}
|
|
19
28
|
{action && (
|
|
20
29
|
<button
|
|
21
30
|
onClick={action.onClick}
|
|
22
31
|
className="mt-3 px-8 py-3 rounded-[14px] border-none bg-accent-bright text-white
|
|
23
32
|
text-[14px] font-600 cursor-pointer active:scale-95 transition-all duration-200
|
|
24
|
-
shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
|
|
25
|
-
style={{ fontFamily: 'inherit' }}
|
|
33
|
+
shadow-[0_4px_16px_rgba(99,102,241,0.2)] relative overflow-hidden group/btn"
|
|
34
|
+
style={{ fontFamily: 'inherit', animation: 'spring-in 0.6s var(--ease-spring) 0.2s both' }}
|
|
26
35
|
>
|
|
27
|
-
{action.label}
|
|
36
|
+
<span className="relative z-10">{action.label}</span>
|
|
37
|
+
<div
|
|
38
|
+
className="absolute inset-0 w-full h-full bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full group-hover/btn:animate-[shimmer-bar_1.5s_ease-in-out_infinite]"
|
|
39
|
+
/>
|
|
28
40
|
</button>
|
|
29
41
|
)}
|
|
30
42
|
</div>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useState, useCallback } from 'react'
|
|
4
|
+
import { createPortal } from 'react-dom'
|
|
5
|
+
import type { CSSProperties } from 'react'
|
|
4
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
7
|
import { useWs } from '@/hooks/use-ws'
|
|
6
8
|
import type { AppNotification } from '@/types'
|
|
@@ -34,6 +36,9 @@ const TYPE_ICON_COLORS: Record<AppNotification['type'], string> = {
|
|
|
34
36
|
error: 'text-red-400',
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
const PANEL_WIDTH = 340
|
|
40
|
+
const PANEL_MARGIN = 8
|
|
41
|
+
|
|
37
42
|
function resolveHttpUrl(raw: string | undefined): string | null {
|
|
38
43
|
if (!raw) return null
|
|
39
44
|
try {
|
|
@@ -56,6 +61,10 @@ export function NotificationCenter({
|
|
|
56
61
|
const [open, setOpen] = useState(false)
|
|
57
62
|
const panelRef = useRef<HTMLDivElement>(null)
|
|
58
63
|
const buttonRef = useRef<HTMLButtonElement>(null)
|
|
64
|
+
const [panelStyle, setPanelStyle] = useState<CSSProperties>({
|
|
65
|
+
left: PANEL_MARGIN,
|
|
66
|
+
top: PANEL_MARGIN,
|
|
67
|
+
})
|
|
59
68
|
|
|
60
69
|
const notifications = useAppStore((s) => s.notifications)
|
|
61
70
|
const unreadCount = useAppStore((s) => s.unreadNotificationCount)
|
|
@@ -102,8 +111,130 @@ export function NotificationCenter({
|
|
|
102
111
|
}
|
|
103
112
|
|
|
104
113
|
const isRow = variant === 'row'
|
|
105
|
-
|
|
106
|
-
const
|
|
114
|
+
|
|
115
|
+
const updatePanelPosition = useCallback(() => {
|
|
116
|
+
if (typeof window === 'undefined' || !buttonRef.current) return
|
|
117
|
+
|
|
118
|
+
const rect = buttonRef.current.getBoundingClientRect()
|
|
119
|
+
const maxLeft = Math.max(PANEL_MARGIN, window.innerWidth - PANEL_WIDTH - PANEL_MARGIN)
|
|
120
|
+
const preferredLeft = align === 'left' ? rect.left : rect.right - PANEL_WIDTH
|
|
121
|
+
const clampedLeft = Math.max(PANEL_MARGIN, Math.min(preferredLeft, maxLeft))
|
|
122
|
+
|
|
123
|
+
if (direction === 'up') {
|
|
124
|
+
setPanelStyle({
|
|
125
|
+
left: clampedLeft,
|
|
126
|
+
bottom: Math.max(PANEL_MARGIN, window.innerHeight - rect.top + PANEL_MARGIN),
|
|
127
|
+
top: undefined,
|
|
128
|
+
})
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
setPanelStyle({
|
|
133
|
+
left: clampedLeft,
|
|
134
|
+
top: Math.max(PANEL_MARGIN, rect.bottom + PANEL_MARGIN),
|
|
135
|
+
bottom: undefined,
|
|
136
|
+
})
|
|
137
|
+
}, [align, direction])
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!open) return
|
|
141
|
+
updatePanelPosition()
|
|
142
|
+
|
|
143
|
+
const handler = () => updatePanelPosition()
|
|
144
|
+
window.addEventListener('resize', handler)
|
|
145
|
+
window.addEventListener('scroll', handler, true)
|
|
146
|
+
return () => {
|
|
147
|
+
window.removeEventListener('resize', handler)
|
|
148
|
+
window.removeEventListener('scroll', handler, true)
|
|
149
|
+
}
|
|
150
|
+
}, [open, updatePanelPosition])
|
|
151
|
+
|
|
152
|
+
const panelNode = open ? (
|
|
153
|
+
<div
|
|
154
|
+
ref={panelRef}
|
|
155
|
+
className="fixed 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-[1200] flex flex-col overflow-hidden"
|
|
156
|
+
style={{
|
|
157
|
+
...panelStyle,
|
|
158
|
+
animation: 'fade-in 0.15s cubic-bezier(0.16, 1, 0.3, 1)',
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
{/* Header */}
|
|
162
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.04] shrink-0">
|
|
163
|
+
<span className="text-[13px] font-600 text-text">Notifications</span>
|
|
164
|
+
<div className="flex items-center gap-2">
|
|
165
|
+
{unreadCount > 0 && (
|
|
166
|
+
<button
|
|
167
|
+
onClick={markAllRead}
|
|
168
|
+
className="text-[11px] font-500 text-text-3 hover:text-text cursor-pointer bg-transparent border-none transition-colors"
|
|
169
|
+
style={{ fontFamily: 'inherit' }}
|
|
170
|
+
>
|
|
171
|
+
Mark all read
|
|
172
|
+
</button>
|
|
173
|
+
)}
|
|
174
|
+
{notifications.some((n) => n.read) && (
|
|
175
|
+
<button
|
|
176
|
+
onClick={clearRead}
|
|
177
|
+
className="text-[11px] font-500 text-text-3 hover:text-text cursor-pointer bg-transparent border-none transition-colors"
|
|
178
|
+
style={{ fontFamily: 'inherit' }}
|
|
179
|
+
>
|
|
180
|
+
Clear read
|
|
181
|
+
</button>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
|
|
186
|
+
{/* List */}
|
|
187
|
+
<div className="flex-1 overflow-y-auto">
|
|
188
|
+
{notifications.length === 0 ? (
|
|
189
|
+
<div className="flex items-center justify-center py-10 text-[13px] text-text-3/50">
|
|
190
|
+
No notifications
|
|
191
|
+
</div>
|
|
192
|
+
) : (
|
|
193
|
+
notifications.map((n) => (
|
|
194
|
+
<button
|
|
195
|
+
key={n.id}
|
|
196
|
+
onClick={() => handleNotificationClick(n)}
|
|
197
|
+
className={`w-full text-left px-4 py-3 border-l-[3px] border-b border-b-white/[0.03] bg-transparent
|
|
198
|
+
hover:bg-white/[0.03] transition-colors cursor-pointer border-t-0 border-r-0
|
|
199
|
+
${TYPE_COLORS[n.type]}
|
|
200
|
+
${n.read ? 'opacity-50' : ''}`}
|
|
201
|
+
style={{ fontFamily: 'inherit' }}
|
|
202
|
+
>
|
|
203
|
+
<div className="flex items-start gap-2.5">
|
|
204
|
+
<span className={`text-[12px] font-700 mt-0.5 shrink-0 w-4 text-center ${TYPE_ICON_COLORS[n.type]}`}>
|
|
205
|
+
{TYPE_ICONS[n.type]}
|
|
206
|
+
</span>
|
|
207
|
+
<div className="flex-1 min-w-0">
|
|
208
|
+
<div className="flex items-center gap-2">
|
|
209
|
+
<span className="text-[12px] font-600 text-text truncate flex-1">{n.title}</span>
|
|
210
|
+
<span className="text-[10px] text-text-3/50 shrink-0">{timeAgo(n.createdAt)}</span>
|
|
211
|
+
</div>
|
|
212
|
+
{n.message && (
|
|
213
|
+
<p className="text-[11px] text-text-3 mt-0.5 leading-relaxed line-clamp-2 m-0">
|
|
214
|
+
{n.message}
|
|
215
|
+
</p>
|
|
216
|
+
)}
|
|
217
|
+
{resolveHttpUrl(n.actionUrl) && (
|
|
218
|
+
<span className="inline-block mt-1 text-[11px] text-accent-bright/90">
|
|
219
|
+
{n.actionLabel || 'Open link'}
|
|
220
|
+
</span>
|
|
221
|
+
)}
|
|
222
|
+
{n.entityType && (
|
|
223
|
+
<span className="inline-block mt-1 text-[10px] text-text-3/40 font-mono">
|
|
224
|
+
{n.entityType}{n.entityId ? `:${n.entityId.slice(0, 8)}` : ''}
|
|
225
|
+
</span>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
{!n.read && (
|
|
229
|
+
<span className="w-2 h-2 rounded-full bg-blue-400 mt-1.5 shrink-0" />
|
|
230
|
+
)}
|
|
231
|
+
</div>
|
|
232
|
+
</button>
|
|
233
|
+
))
|
|
234
|
+
)}
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
) : null
|
|
107
238
|
|
|
108
239
|
return (
|
|
109
240
|
<div className="relative">
|
|
@@ -134,90 +265,7 @@ export function NotificationCenter({
|
|
|
134
265
|
</span>
|
|
135
266
|
)}
|
|
136
267
|
</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
|
-
)}
|
|
268
|
+
{panelNode && typeof document !== 'undefined' ? createPortal(panelNode, document.body) : panelNode}
|
|
221
269
|
</div>
|
|
222
270
|
)
|
|
223
271
|
}
|
|
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
6
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
7
7
|
import { api } from '@/lib/api-client'
|
|
8
|
+
import { toast } from 'sonner'
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
10
11
|
open: boolean
|
|
@@ -39,7 +40,10 @@ export function ProfileSheet({ open, onClose }: Props) {
|
|
|
39
40
|
})
|
|
40
41
|
setUser(trimmed.toLowerCase())
|
|
41
42
|
await loadSettings()
|
|
43
|
+
toast.success('Profile updated')
|
|
42
44
|
onClose()
|
|
45
|
+
} catch (err: unknown) {
|
|
46
|
+
toast.error(err instanceof Error ? err.message : 'Failed to update profile')
|
|
43
47
|
} finally {
|
|
44
48
|
setSaving(false)
|
|
45
49
|
}
|