@swarmclawai/swarmclaw 0.5.3 → 0.6.2
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 +53 -9
- package/bin/server-cmd.js +1 -0
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +5 -2
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +284 -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/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/files/open/route.ts +43 -0
- 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/route.ts +4 -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 +53 -1
- 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/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- 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 +12 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +18 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +63 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +40 -1
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +42 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +50 -17
- package/src/components/agents/agent-chat-list.tsx +148 -12
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +120 -65
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- 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/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +173 -0
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +457 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +146 -0
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +356 -315
- package/src/components/chat/message-list.tsx +230 -8
- package/src/components/chat/streaming-bubble.tsx +104 -47
- 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 +111 -73
- 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 +130 -0
- package/src/components/chatrooms/chatroom-message.tsx +432 -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-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +95 -56
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +107 -43
- 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 +194 -97
- 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 +1 -1
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +259 -126
- 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/settings/gateway-disconnect-overlay.tsx +80 -0
- 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/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- 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/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +44 -6
- 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 +31 -15
- 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-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- 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-storage.tsx +206 -0
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +57 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +182 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- 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 +59 -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 +416 -74
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-view-router.ts +69 -19
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +75 -15
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +229 -7
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +14 -2
- package/src/lib/server/daemon-state.ts +157 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +48 -6
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +105 -0
- package/src/lib/server/memory-db.ts +183 -10
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +56 -10
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +10 -0
- package/src/lib/server/session-tools/memory.ts +7 -1
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/storage.ts +53 -29
- package/src/lib/server/stream-agent-chat.ts +185 -57
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/stores/use-app-store.ts +41 -3
- package/src/stores/use-chat-store.ts +113 -5
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +88 -4
package/src/app/page.tsx
CHANGED
|
@@ -12,6 +12,130 @@ import { SetupWizard } from '@/components/auth/setup-wizard'
|
|
|
12
12
|
import { AppLayout } from '@/components/layout/app-layout'
|
|
13
13
|
import { useViewRouter } from '@/hooks/use-view-router'
|
|
14
14
|
|
|
15
|
+
function FullScreenLoader() {
|
|
16
|
+
return (
|
|
17
|
+
<div className="h-full flex flex-col items-center justify-center bg-bg overflow-hidden select-none">
|
|
18
|
+
{/* Animated orbital ring */}
|
|
19
|
+
<div className="relative w-[120px] h-[120px] mb-8">
|
|
20
|
+
{/* Outer glow pulse */}
|
|
21
|
+
<div
|
|
22
|
+
className="absolute inset-[-20px] rounded-full"
|
|
23
|
+
style={{
|
|
24
|
+
background: 'radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%)',
|
|
25
|
+
animation: 'sc-glow 2.5s ease-in-out infinite',
|
|
26
|
+
}}
|
|
27
|
+
/>
|
|
28
|
+
|
|
29
|
+
{/* Orbital ring */}
|
|
30
|
+
<div
|
|
31
|
+
className="absolute inset-0 rounded-full border border-white/[0.06]"
|
|
32
|
+
style={{ animation: 'sc-ring 3s linear infinite' }}
|
|
33
|
+
/>
|
|
34
|
+
|
|
35
|
+
{/* Orbiting dots */}
|
|
36
|
+
{[0, 1, 2, 3, 4, 5].map((i) => (
|
|
37
|
+
<div
|
|
38
|
+
key={i}
|
|
39
|
+
className="absolute inset-0"
|
|
40
|
+
style={{
|
|
41
|
+
animation: `sc-orbit 2.4s cubic-bezier(0.4, 0, 0.2, 1) infinite`,
|
|
42
|
+
animationDelay: `${i * -0.4}s`,
|
|
43
|
+
}}
|
|
44
|
+
>
|
|
45
|
+
<div
|
|
46
|
+
className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
|
|
47
|
+
style={{
|
|
48
|
+
width: i === 0 ? 8 : 6,
|
|
49
|
+
height: i === 0 ? 8 : 6,
|
|
50
|
+
background: i === 0 ? '#818CF8' : `rgba(129, 140, 248, ${0.7 - i * 0.1})`,
|
|
51
|
+
boxShadow: i === 0 ? '0 0 12px rgba(99,102,241,0.5)' : 'none',
|
|
52
|
+
}}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
))}
|
|
56
|
+
|
|
57
|
+
{/* Center logo mark */}
|
|
58
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
59
|
+
<div
|
|
60
|
+
className="relative"
|
|
61
|
+
style={{ animation: 'sc-breathe 2.5s ease-in-out infinite' }}
|
|
62
|
+
>
|
|
63
|
+
<svg width="36" height="36" viewBox="0 0 36 36" fill="none">
|
|
64
|
+
{/* Hexagonal claw mark */}
|
|
65
|
+
<path
|
|
66
|
+
d="M18 4L30 11V25L18 32L6 25V11L18 4Z"
|
|
67
|
+
stroke="rgba(129, 140, 248, 0.3)"
|
|
68
|
+
strokeWidth="1"
|
|
69
|
+
fill="none"
|
|
70
|
+
/>
|
|
71
|
+
<path
|
|
72
|
+
d="M18 9L25 13V23L18 27L11 23V13L18 9Z"
|
|
73
|
+
stroke="rgba(129, 140, 248, 0.5)"
|
|
74
|
+
strokeWidth="1.5"
|
|
75
|
+
fill="rgba(99, 102, 241, 0.06)"
|
|
76
|
+
/>
|
|
77
|
+
{/* Claw lines */}
|
|
78
|
+
<path d="M14 15L18 20L22 15" stroke="#818CF8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
|
79
|
+
<path d="M12 13L18 20L24 13" stroke="rgba(129, 140, 248, 0.3)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
80
|
+
</svg>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Brand text */}
|
|
86
|
+
<div
|
|
87
|
+
className="text-[15px] font-display font-700 tracking-[0.15em] uppercase"
|
|
88
|
+
style={{
|
|
89
|
+
background: 'linear-gradient(135deg, rgba(255,255,255,0.6), rgba(129, 140, 248, 0.8))',
|
|
90
|
+
WebkitBackgroundClip: 'text',
|
|
91
|
+
WebkitTextFillColor: 'transparent',
|
|
92
|
+
animation: 'sc-text-fade 2s ease-in-out infinite alternate',
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
SwarmClaw
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
{/* Loading bar */}
|
|
99
|
+
<div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden">
|
|
100
|
+
<div
|
|
101
|
+
className="h-full rounded-full bg-accent-bright/60"
|
|
102
|
+
style={{ animation: 'sc-progress 1.5s ease-in-out infinite' }}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{/* Loading animation keyframes */}
|
|
107
|
+
<style>{`
|
|
108
|
+
@keyframes sc-orbit {
|
|
109
|
+
from { transform: rotate(0deg); }
|
|
110
|
+
to { transform: rotate(360deg); }
|
|
111
|
+
}
|
|
112
|
+
@keyframes sc-ring {
|
|
113
|
+
from { transform: rotate(0deg) scale(1); }
|
|
114
|
+
50% { transform: rotate(180deg) scale(1.02); }
|
|
115
|
+
to { transform: rotate(360deg) scale(1); }
|
|
116
|
+
}
|
|
117
|
+
@keyframes sc-breathe {
|
|
118
|
+
0%, 100% { transform: scale(1); opacity: 0.9; }
|
|
119
|
+
50% { transform: scale(1.06); opacity: 1; }
|
|
120
|
+
}
|
|
121
|
+
@keyframes sc-glow {
|
|
122
|
+
0%, 100% { opacity: 0.5; transform: scale(0.9); }
|
|
123
|
+
50% { opacity: 1; transform: scale(1.1); }
|
|
124
|
+
}
|
|
125
|
+
@keyframes sc-text-fade {
|
|
126
|
+
0% { opacity: 0.6; }
|
|
127
|
+
100% { opacity: 1; }
|
|
128
|
+
}
|
|
129
|
+
@keyframes sc-progress {
|
|
130
|
+
0% { width: 0; margin-left: 0; }
|
|
131
|
+
50% { width: 70%; margin-left: 15%; }
|
|
132
|
+
100% { width: 0; margin-left: 100%; }
|
|
133
|
+
}
|
|
134
|
+
`}</style>
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
15
139
|
export default function Home() {
|
|
16
140
|
const currentUser = useAppStore((s) => s.currentUser)
|
|
17
141
|
const setUser = useAppStore((s) => s.setUser)
|
|
@@ -83,26 +207,31 @@ export default function Home() {
|
|
|
83
207
|
|
|
84
208
|
useWs('sessions', loadSessions, 5000)
|
|
85
209
|
|
|
86
|
-
// Auto-select
|
|
210
|
+
// Auto-select agent's thread on load — resolves a persisted agentId into a session,
|
|
211
|
+
// or falls back to defaultAgentId from settings, then first agent.
|
|
212
|
+
const [agentReady, setAgentReady] = useState(false)
|
|
87
213
|
useEffect(() => {
|
|
88
214
|
if (!authenticated || !currentUser) return
|
|
89
|
-
const state = useAppStore.getState()
|
|
90
|
-
// Only auto-select if no agent is selected yet
|
|
91
|
-
if (state.currentAgentId) return
|
|
92
|
-
|
|
93
|
-
// Load agents and select 'default' agent
|
|
94
215
|
let cancelled = false
|
|
95
216
|
;(async () => {
|
|
96
217
|
try {
|
|
218
|
+
const state = useAppStore.getState()
|
|
97
219
|
await state.loadAgents()
|
|
98
220
|
if (cancelled) return
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
221
|
+
|
|
222
|
+
const { agents, currentAgentId, appSettings } = useAppStore.getState()
|
|
223
|
+
// Priority: persisted agent > settings default > first agent
|
|
224
|
+
const targetId = (currentAgentId && agents[currentAgentId])
|
|
225
|
+
? currentAgentId
|
|
226
|
+
: (appSettings.defaultAgentId && agents[appSettings.defaultAgentId])
|
|
227
|
+
? appSettings.defaultAgentId
|
|
228
|
+
: Object.values(agents)[0]?.id || null
|
|
229
|
+
|
|
230
|
+
if (targetId) {
|
|
231
|
+
await useAppStore.getState().setCurrentAgent(targetId)
|
|
104
232
|
}
|
|
105
233
|
} catch { /* ignore */ }
|
|
234
|
+
if (!cancelled) setAgentReady(true)
|
|
106
235
|
})()
|
|
107
236
|
return () => { cancelled = true }
|
|
108
237
|
}, [authenticated, currentUser])
|
|
@@ -148,10 +277,10 @@ export default function Home() {
|
|
|
148
277
|
|
|
149
278
|
useViewRouter()
|
|
150
279
|
|
|
151
|
-
if (!hydrated || !authChecked) return
|
|
280
|
+
if (!hydrated || !authChecked) return <FullScreenLoader />
|
|
152
281
|
if (!authenticated) return <AccessKeyGate onAuthenticated={() => setAuthenticated(true)} />
|
|
153
282
|
if (!currentUser) return <UserPicker />
|
|
154
|
-
if (setupDone === null) return
|
|
283
|
+
if (setupDone === null || !agentReady) return <FullScreenLoader />
|
|
155
284
|
if (!setupDone) return <SetupWizard onComplete={() => setSetupDone(true)} />
|
|
156
285
|
return <AppLayout />
|
|
157
286
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -56,6 +56,37 @@ const COMMAND_GROUPS = [
|
|
|
56
56
|
cmd('install', 'POST', '/clawhub/install', 'Install a skill from ClawHub', { expectsJsonBody: true }),
|
|
57
57
|
],
|
|
58
58
|
},
|
|
59
|
+
{
|
|
60
|
+
name: 'chatrooms',
|
|
61
|
+
description: 'Manage multi-agent chatrooms',
|
|
62
|
+
commands: [
|
|
63
|
+
cmd('list', 'GET', '/chatrooms', 'List chatrooms'),
|
|
64
|
+
cmd('get', 'GET', '/chatrooms/:id', 'Get chatroom by id'),
|
|
65
|
+
cmd('create', 'POST', '/chatrooms', 'Create a chatroom', { expectsJsonBody: true }),
|
|
66
|
+
cmd('update', 'PUT', '/chatrooms/:id', 'Update a chatroom', { expectsJsonBody: true }),
|
|
67
|
+
cmd('delete', 'DELETE', '/chatrooms/:id', 'Delete a chatroom'),
|
|
68
|
+
cmd('chat', 'POST', '/chatrooms/:id/chat', 'Post a message to a chatroom and stream agent replies', {
|
|
69
|
+
expectsJsonBody: true,
|
|
70
|
+
responseType: 'sse',
|
|
71
|
+
}),
|
|
72
|
+
cmd('add-member', 'POST', '/chatrooms/:id/members', 'Add an agent to a chatroom (use --data \'{"agentId":"..."}\')', { expectsJsonBody: true }),
|
|
73
|
+
cmd('remove-member', 'DELETE', '/chatrooms/:id/members', 'Remove an agent from a chatroom (use --data \'{"agentId":"..."}\')', { expectsJsonBody: true }),
|
|
74
|
+
cmd('react', 'POST', '/chatrooms/:id/reactions', 'Toggle a reaction on a chatroom message', {
|
|
75
|
+
expectsJsonBody: true,
|
|
76
|
+
}),
|
|
77
|
+
cmd('pin', 'POST', '/chatrooms/:id/pins', 'Toggle pin on a chatroom message', {
|
|
78
|
+
expectsJsonBody: true,
|
|
79
|
+
}),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'canvas',
|
|
84
|
+
description: 'Read/update per-session canvas content',
|
|
85
|
+
commands: [
|
|
86
|
+
cmd('get', 'GET', '/canvas/:sessionId', 'Get current canvas content for a session'),
|
|
87
|
+
cmd('set', 'POST', '/canvas/:sessionId', 'Set/clear canvas content for a session', { expectsJsonBody: true }),
|
|
88
|
+
],
|
|
89
|
+
},
|
|
59
90
|
{
|
|
60
91
|
name: 'connectors',
|
|
61
92
|
description: 'Manage chat connectors',
|
|
@@ -131,6 +162,7 @@ const COMMAND_GROUPS = [
|
|
|
131
162
|
description: 'Serve and manage local files',
|
|
132
163
|
commands: [
|
|
133
164
|
cmd('serve', 'GET', '/files/serve', 'Serve a local file (use --query path=/abs/path)'),
|
|
165
|
+
cmd('open', 'POST', '/files/open', 'Open a local file path via the host default app/browser', { expectsJsonBody: true }),
|
|
134
166
|
],
|
|
135
167
|
},
|
|
136
168
|
{
|
|
@@ -358,6 +390,8 @@ const COMMAND_GROUPS = [
|
|
|
358
390
|
}),
|
|
359
391
|
cmd('messages', 'GET', '/sessions/:id/messages', 'Get session messages'),
|
|
360
392
|
cmd('messages-update', 'PUT', '/sessions/:id/messages', 'Update session message metadata (e.g. bookmark)', { expectsJsonBody: true }),
|
|
393
|
+
cmd('messages-send', 'POST', '/sessions/:id/messages', 'Append a user/system message to a session', { expectsJsonBody: true }),
|
|
394
|
+
cmd('messages-delete', 'DELETE', '/sessions/:id/messages', 'Delete a message from a session', { expectsJsonBody: true }),
|
|
361
395
|
cmd('fork', 'POST', '/sessions/:id/fork', 'Fork session from a specific message index', { expectsJsonBody: true }),
|
|
362
396
|
cmd('edit-resend', 'POST', '/sessions/:id/edit-resend', 'Edit and resend from a specific message index', { expectsJsonBody: true }),
|
|
363
397
|
cmd('main-loop', 'GET', '/sessions/:id/main-loop', 'Get main mission loop state'),
|
|
@@ -427,10 +461,12 @@ const COMMAND_GROUPS = [
|
|
|
427
461
|
cmd('list', 'GET', '/tasks', 'List tasks'),
|
|
428
462
|
cmd('get', 'GET', '/tasks/:id', 'Get task'),
|
|
429
463
|
cmd('create', 'POST', '/tasks', 'Create task', { expectsJsonBody: true }),
|
|
464
|
+
cmd('bulk', 'POST', '/tasks/bulk', 'Bulk update tasks (status/agent/project)', { expectsJsonBody: true }),
|
|
430
465
|
cmd('update', 'PUT', '/tasks/:id', 'Update task', { expectsJsonBody: true }),
|
|
431
466
|
cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'),
|
|
432
467
|
cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }),
|
|
433
468
|
cmd('approve', 'POST', '/tasks/:id/approve', 'Approve or reject a pending tool execution', { expectsJsonBody: true }),
|
|
469
|
+
cmd('metrics', 'GET', '/tasks/metrics', 'Get task board metrics (supports --query range=24h|7d|30d)'),
|
|
434
470
|
],
|
|
435
471
|
},
|
|
436
472
|
{
|
|
@@ -461,9 +497,12 @@ const COMMAND_GROUPS = [
|
|
|
461
497
|
},
|
|
462
498
|
{
|
|
463
499
|
name: 'uploads',
|
|
464
|
-
description: '
|
|
500
|
+
description: 'Manage uploaded artifacts',
|
|
465
501
|
commands: [
|
|
502
|
+
cmd('list', 'GET', '/uploads', 'List uploaded artifacts'),
|
|
466
503
|
cmd('get', 'GET', '/uploads/:filename', 'Download uploaded artifact', { responseType: 'binary' }),
|
|
504
|
+
cmd('delete', 'DELETE', '/uploads/:filename', 'Delete uploaded artifact by filename'),
|
|
505
|
+
cmd('delete-many', 'DELETE', '/uploads', 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', { expectsJsonBody: true }),
|
|
467
506
|
],
|
|
468
507
|
},
|
|
469
508
|
{
|
package/src/cli/index.test.js
CHANGED
|
@@ -81,6 +81,36 @@ test('CLI command map covers all API route method/path pairs', () => {
|
|
|
81
81
|
assert.deepEqual(missing, [])
|
|
82
82
|
})
|
|
83
83
|
|
|
84
|
+
test('Binary CLI router reaches every mapped API command pair', async () => {
|
|
85
|
+
const { shouldUseLegacyTsCli, TS_CLI_ACTIONS } = await import('../../bin/swarmclaw.js')
|
|
86
|
+
|
|
87
|
+
for (const command of COMMANDS) {
|
|
88
|
+
if (command.virtual) continue
|
|
89
|
+
|
|
90
|
+
const pathArgs = extractPathParams(command.route).map((name, index) => `${name}-${index + 1}`)
|
|
91
|
+
const routedToLegacyTs = shouldUseLegacyTsCli([command.group, command.action, ...pathArgs])
|
|
92
|
+
|
|
93
|
+
if (routedToLegacyTs) {
|
|
94
|
+
assert.ok(
|
|
95
|
+
TS_CLI_ACTIONS[command.group]?.has(command.action),
|
|
96
|
+
`legacy TS router should only claim known actions (${command.group} ${command.action})`,
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Spot-check known API commands that are map-only today.
|
|
102
|
+
assert.equal(shouldUseLegacyTsCli(['chatrooms', 'list']), false)
|
|
103
|
+
assert.equal(shouldUseLegacyTsCli(['tasks', 'approve', 'task-1']), false)
|
|
104
|
+
|
|
105
|
+
// Help paths should route to mapped CLI for full command discoverability.
|
|
106
|
+
assert.equal(shouldUseLegacyTsCli([]), false)
|
|
107
|
+
assert.equal(shouldUseLegacyTsCli(['--help']), false)
|
|
108
|
+
assert.equal(shouldUseLegacyTsCli(['tasks', '--help']), false)
|
|
109
|
+
|
|
110
|
+
// And a legacy command that should remain on the richer TS path.
|
|
111
|
+
assert.equal(shouldUseLegacyTsCli(['tasks', 'create']), true)
|
|
112
|
+
})
|
|
113
|
+
|
|
84
114
|
test('parseArgv parses group/action/options', () => {
|
|
85
115
|
const parsed = parseArgv([
|
|
86
116
|
'runs',
|
package/src/cli/spec.js
CHANGED
|
@@ -25,6 +25,28 @@ const COMMAND_GROUPS = {
|
|
|
25
25
|
login: { description: 'Validate an access key', method: 'POST', path: '/auth' },
|
|
26
26
|
},
|
|
27
27
|
},
|
|
28
|
+
chatrooms: {
|
|
29
|
+
description: 'Manage multi-agent chatrooms',
|
|
30
|
+
commands: {
|
|
31
|
+
list: { description: 'List chatrooms', method: 'GET', path: '/chatrooms' },
|
|
32
|
+
get: { description: 'Get chatroom by id', method: 'GET', path: '/chatrooms/:id', params: ['id'] },
|
|
33
|
+
create: { description: 'Create a chatroom', method: 'POST', path: '/chatrooms' },
|
|
34
|
+
update: { description: 'Update a chatroom', method: 'PUT', path: '/chatrooms/:id', params: ['id'] },
|
|
35
|
+
delete: { description: 'Delete a chatroom', method: 'DELETE', path: '/chatrooms/:id', params: ['id'] },
|
|
36
|
+
chat: { description: 'Post chatroom message and stream agent replies', method: 'POST', path: '/chatrooms/:id/chat', params: ['id'] },
|
|
37
|
+
'add-member': { description: 'Add an agent to a chatroom', method: 'POST', path: '/chatrooms/:id/members', params: ['id'] },
|
|
38
|
+
'remove-member': { description: 'Remove an agent from a chatroom', method: 'DELETE', path: '/chatrooms/:id/members', params: ['id'] },
|
|
39
|
+
react: { description: 'Toggle reaction on a chatroom message', method: 'POST', path: '/chatrooms/:id/reactions', params: ['id'] },
|
|
40
|
+
pin: { description: 'Toggle pin on a chatroom message', method: 'POST', path: '/chatrooms/:id/pins', params: ['id'] },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
canvas: {
|
|
44
|
+
description: 'Session canvas content',
|
|
45
|
+
commands: {
|
|
46
|
+
get: { description: 'Get current canvas content for a session', method: 'GET', path: '/canvas/:sessionId', params: ['sessionId'] },
|
|
47
|
+
set: { description: 'Set/clear canvas content for a session', method: 'POST', path: '/canvas/:sessionId', params: ['sessionId'] },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
28
50
|
connectors: {
|
|
29
51
|
description: 'Manage chat connectors',
|
|
30
52
|
commands: {
|
|
@@ -105,6 +127,22 @@ const COMMAND_GROUPS = {
|
|
|
105
127
|
},
|
|
106
128
|
},
|
|
107
129
|
},
|
|
130
|
+
uploads: {
|
|
131
|
+
description: 'Manage uploaded artifacts',
|
|
132
|
+
commands: {
|
|
133
|
+
list: { description: 'List uploaded artifacts', method: 'GET', path: '/uploads' },
|
|
134
|
+
get: { description: 'Download uploaded artifact by filename', method: 'GET', path: '/uploads/:filename', params: ['filename'], binary: true },
|
|
135
|
+
delete: { description: 'Delete uploaded artifact by filename', method: 'DELETE', path: '/uploads/:filename', params: ['filename'] },
|
|
136
|
+
'delete-many': { description: 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', method: 'DELETE', path: '/uploads' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
files: {
|
|
140
|
+
description: 'Serve/open local files',
|
|
141
|
+
commands: {
|
|
142
|
+
serve: { description: 'Serve a local file (supports --query path=/some/file)', method: 'GET', path: '/files/serve' },
|
|
143
|
+
open: { description: 'Open a local file path via host default app/browser', method: 'POST', path: '/files/open' },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
108
146
|
logs: {
|
|
109
147
|
description: 'Application logs',
|
|
110
148
|
commands: {
|
|
@@ -247,6 +285,8 @@ const COMMAND_GROUPS = {
|
|
|
247
285
|
'heartbeat-disable-all': { description: 'Disable all session heartbeats and cancel queued heartbeat runs', method: 'POST', path: '/sessions/heartbeat' },
|
|
248
286
|
messages: { description: 'Get session message history', method: 'GET', path: '/sessions/:id/messages', params: ['id'] },
|
|
249
287
|
'messages-update': { description: 'Update session message metadata (e.g. bookmark)', method: 'PUT', path: '/sessions/:id/messages', params: ['id'] },
|
|
288
|
+
'messages-send': { description: 'Append a user/system message to a session', method: 'POST', path: '/sessions/:id/messages', params: ['id'] },
|
|
289
|
+
'messages-delete': { description: 'Delete a message from a session', method: 'DELETE', path: '/sessions/:id/messages', params: ['id'] },
|
|
250
290
|
fork: { description: 'Fork session from a specific message index', method: 'POST', path: '/sessions/:id/fork', params: ['id'] },
|
|
251
291
|
'edit-resend': { description: 'Edit and resend from a specific message index', method: 'POST', path: '/sessions/:id/edit-resend', params: ['id'] },
|
|
252
292
|
'main-loop': { description: 'Get main mission loop state for a session', method: 'GET', path: '/sessions/:id/main-loop', params: ['id'] },
|
|
@@ -303,10 +343,12 @@ const COMMAND_GROUPS = {
|
|
|
303
343
|
list: { description: 'List tasks', method: 'GET', path: '/tasks' },
|
|
304
344
|
get: { description: 'Get task by id', method: 'GET', path: '/tasks/:id', params: ['id'] },
|
|
305
345
|
create: { description: 'Create task', method: 'POST', path: '/tasks' },
|
|
346
|
+
bulk: { description: 'Bulk update tasks (status/agent/project)', method: 'POST', path: '/tasks/bulk' },
|
|
306
347
|
update: { description: 'Update task', method: 'PUT', path: '/tasks/:id', params: ['id'] },
|
|
307
348
|
delete: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
|
|
308
349
|
archive: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
|
|
309
350
|
approve: { description: 'Approve or reject a pending tool execution', method: 'POST', path: '/tasks/:id/approve', params: ['id'] },
|
|
351
|
+
metrics: { description: 'Get task board metrics (supports --query range=24h|7d|30d)', method: 'GET', path: '/tasks/metrics' },
|
|
310
352
|
},
|
|
311
353
|
},
|
|
312
354
|
webhooks: {
|
|
@@ -3,26 +3,66 @@
|
|
|
3
3
|
import { useMemo } from 'react'
|
|
4
4
|
import multiavatar from '@multiavatar/multiavatar'
|
|
5
5
|
|
|
6
|
+
/** Strip scripts/event handlers from SVG to prevent XSS */
|
|
7
|
+
function sanitizeSvg(svg: string): string {
|
|
8
|
+
return svg
|
|
9
|
+
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
|
10
|
+
.replace(/\bon\w+\s*=\s*"[^"]*"/gi, '')
|
|
11
|
+
.replace(/\bon\w+\s*=\s*'[^']*'/gi, '')
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
interface Props {
|
|
7
15
|
seed?: string | null
|
|
8
16
|
name: string
|
|
9
17
|
size?: number
|
|
10
18
|
className?: string
|
|
19
|
+
status?: 'idle' | 'busy' | 'online'
|
|
20
|
+
heartbeatPulse?: boolean
|
|
11
21
|
}
|
|
12
22
|
|
|
13
|
-
|
|
23
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
24
|
+
busy: 'bg-amber-400',
|
|
25
|
+
online: 'bg-emerald-400',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const HEART_PATH = 'M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z'
|
|
29
|
+
|
|
30
|
+
export function AgentAvatar({ seed, name, size = 32, className = '', status, heartbeatPulse }: Props) {
|
|
14
31
|
const svgHtml = useMemo(() => {
|
|
15
32
|
if (!seed) return null
|
|
16
|
-
return multiavatar(seed)
|
|
33
|
+
return sanitizeSvg(multiavatar(seed))
|
|
17
34
|
}, [seed])
|
|
18
35
|
|
|
36
|
+
const dotSize = Math.max(6, Math.round(size * 0.28))
|
|
37
|
+
const dot = status && status !== 'idle' ? (
|
|
38
|
+
<span
|
|
39
|
+
className={`absolute -bottom-0.5 -right-0.5 rounded-full ${STATUS_COLORS[status]} ring-2 ring-[#0f0f1a]`}
|
|
40
|
+
style={{ width: dotSize, height: dotSize }}
|
|
41
|
+
title={status === 'busy' ? 'Busy' : 'Online'}
|
|
42
|
+
/>
|
|
43
|
+
) : null
|
|
44
|
+
|
|
45
|
+
const heartEl = heartbeatPulse ? (
|
|
46
|
+
<svg
|
|
47
|
+
className="absolute left-1/2 -translate-x-1/2 pointer-events-none"
|
|
48
|
+
style={{ top: -Math.max(10, size * 0.35), width: 10, height: 10, animation: 'heartbeat-float 1.5s ease forwards' }}
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
fill="#22c55e"
|
|
51
|
+
>
|
|
52
|
+
<path d={HEART_PATH} />
|
|
53
|
+
</svg>
|
|
54
|
+
) : null
|
|
55
|
+
|
|
19
56
|
if (svgHtml) {
|
|
20
57
|
return (
|
|
21
|
-
<div
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
58
|
+
<div className={`relative shrink-0 ${className}`} style={{ width: size, height: size }}>
|
|
59
|
+
<div
|
|
60
|
+
className="rounded-full overflow-hidden w-full h-full"
|
|
61
|
+
dangerouslySetInnerHTML={{ __html: svgHtml }}
|
|
62
|
+
/>
|
|
63
|
+
{heartEl}
|
|
64
|
+
{dot}
|
|
65
|
+
</div>
|
|
26
66
|
)
|
|
27
67
|
}
|
|
28
68
|
|
|
@@ -36,10 +76,17 @@ export function AgentAvatar({ seed, name, size = 32, className = '' }: Props) {
|
|
|
36
76
|
|
|
37
77
|
return (
|
|
38
78
|
<div
|
|
39
|
-
className={`shrink-0
|
|
40
|
-
style={{ width: size, height: size
|
|
79
|
+
className={`relative shrink-0 ${className}`}
|
|
80
|
+
style={{ width: size, height: size }}
|
|
41
81
|
>
|
|
42
|
-
|
|
82
|
+
<div
|
|
83
|
+
className="rounded-full flex items-center justify-center bg-accent-soft text-accent-bright font-600 w-full h-full"
|
|
84
|
+
style={{ fontSize: size * 0.38 }}
|
|
85
|
+
>
|
|
86
|
+
{initials || '?'}
|
|
87
|
+
</div>
|
|
88
|
+
{heartEl}
|
|
89
|
+
{dot}
|
|
43
90
|
</div>
|
|
44
91
|
)
|
|
45
92
|
}
|
|
@@ -4,6 +4,7 @@ import { useState } from 'react'
|
|
|
4
4
|
import type { Agent } from '@/types'
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
6
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
7
|
+
import { useWs } from '@/hooks/use-ws'
|
|
7
8
|
import { api } from '@/lib/api-client'
|
|
8
9
|
import { createAgent, deleteAgent } from '@/lib/agents'
|
|
9
10
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
|
@@ -17,16 +18,18 @@ import {
|
|
|
17
18
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
18
19
|
import { useApprovalStore } from '@/stores/use-approval-store'
|
|
19
20
|
import { AgentAvatar } from './agent-avatar'
|
|
21
|
+
import { toast } from 'sonner'
|
|
20
22
|
|
|
21
23
|
interface Props {
|
|
22
24
|
agent: Agent
|
|
23
25
|
isDefault?: boolean
|
|
24
26
|
isRunning?: boolean
|
|
27
|
+
isOnline?: boolean
|
|
25
28
|
isSelected?: boolean
|
|
26
29
|
onSetDefault?: (id: string) => void
|
|
27
30
|
}
|
|
28
31
|
|
|
29
|
-
export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefault }: Props) {
|
|
32
|
+
export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, onSetDefault }: Props) {
|
|
30
33
|
const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
|
|
31
34
|
const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
|
|
32
35
|
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
@@ -34,12 +37,18 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
|
|
|
34
37
|
const setCurrentSession = useAppStore((s) => s.setCurrentSession)
|
|
35
38
|
const setActiveView = useAppStore((s) => s.setActiveView)
|
|
36
39
|
const setMessages = useChatStore((s) => s.setMessages)
|
|
40
|
+
const togglePinAgent = useAppStore((s) => s.togglePinAgent)
|
|
37
41
|
const [running, setRunning] = useState(false)
|
|
38
42
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
39
43
|
const [taskInput, setTaskInput] = useState('')
|
|
40
44
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
41
45
|
const approvals = useApprovalStore((s) => s.approvals)
|
|
42
46
|
const pendingApprovalCount = Object.values(approvals).filter((a) => a.agentId === agent.id).length
|
|
47
|
+
const [heartbeatPulse, setHeartbeatPulse] = useState(false)
|
|
48
|
+
useWs(`heartbeat:agent:${agent.id}`, () => {
|
|
49
|
+
setHeartbeatPulse(true)
|
|
50
|
+
setTimeout(() => setHeartbeatPulse(false), 1500)
|
|
51
|
+
})
|
|
43
52
|
|
|
44
53
|
const handleClick = () => {
|
|
45
54
|
setEditingAgentId(agent.id)
|
|
@@ -74,11 +83,13 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
|
|
|
74
83
|
const { id: _id, createdAt: _ca, updatedAt: _ua, ...rest } = agent
|
|
75
84
|
await createAgent({ ...rest, name: agent.name + ' (Copy)' })
|
|
76
85
|
await loadAgents()
|
|
86
|
+
toast.success('Agent duplicated')
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
const handleDelete = async () => {
|
|
80
90
|
await deleteAgent(agent.id)
|
|
81
91
|
await loadAgents()
|
|
92
|
+
toast.success('Agent moved to trash')
|
|
82
93
|
setConfirmDelete(false)
|
|
83
94
|
}
|
|
84
95
|
|
|
@@ -93,6 +104,21 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
|
|
|
93
104
|
: 'bg-transparent border border-transparent hover:bg-white/[0.05] hover:border-white/[0.08]'}`}
|
|
94
105
|
>
|
|
95
106
|
{isSelected && <div className="card-select-indicator" />}
|
|
107
|
+
{/* Pin/star button */}
|
|
108
|
+
<button
|
|
109
|
+
onClick={(e) => {
|
|
110
|
+
e.stopPropagation()
|
|
111
|
+
togglePinAgent(agent.id)
|
|
112
|
+
toast.success(agent.pinned ? 'Agent unpinned' : 'Agent pinned')
|
|
113
|
+
}}
|
|
114
|
+
aria-label={agent.pinned ? 'Unpin agent' : 'Pin agent'}
|
|
115
|
+
className={`absolute top-3 right-10 p-1 rounded-[6px] transition-all bg-transparent border-none cursor-pointer hover:bg-white/[0.06]
|
|
116
|
+
${agent.pinned ? 'opacity-100 text-amber-400' : 'opacity-0 group-hover:opacity-60 hover:!opacity-100 text-text-3'}`}
|
|
117
|
+
>
|
|
118
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill={agent.pinned ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
119
|
+
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
|
120
|
+
</svg>
|
|
121
|
+
</button>
|
|
96
122
|
{/* Three-dot dropdown */}
|
|
97
123
|
<DropdownMenu>
|
|
98
124
|
<DropdownMenuTrigger asChild>
|
|
@@ -111,9 +137,12 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
|
|
|
111
137
|
</DropdownMenuTrigger>
|
|
112
138
|
<DropdownMenuContent align="end" className="min-w-[140px]">
|
|
113
139
|
<DropdownMenuItem onClick={handleClick}>Edit</DropdownMenuItem>
|
|
140
|
+
<DropdownMenuItem onClick={() => { togglePinAgent(agent.id); toast.success(agent.pinned ? 'Agent unpinned' : 'Agent pinned') }}>
|
|
141
|
+
{agent.pinned ? 'Unpin' : 'Pin'}
|
|
142
|
+
</DropdownMenuItem>
|
|
114
143
|
<DropdownMenuItem onClick={handleDuplicate}>Duplicate</DropdownMenuItem>
|
|
115
144
|
{!isDefault && onSetDefault && (
|
|
116
|
-
<DropdownMenuItem onClick={() => onSetDefault(agent.id)}>Set Default</DropdownMenuItem>
|
|
145
|
+
<DropdownMenuItem onClick={() => { onSetDefault(agent.id); toast.success(`${agent.name} set as default`) }}>Set Default</DropdownMenuItem>
|
|
117
146
|
)}
|
|
118
147
|
<DropdownMenuSeparator />
|
|
119
148
|
<DropdownMenuItem
|
|
@@ -126,14 +155,17 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
|
|
|
126
155
|
</DropdownMenu>
|
|
127
156
|
|
|
128
157
|
<div className="flex items-center gap-2.5">
|
|
129
|
-
<AgentAvatar
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
158
|
+
<AgentAvatar
|
|
159
|
+
seed={agent.avatarSeed}
|
|
160
|
+
name={agent.name}
|
|
161
|
+
size={28}
|
|
162
|
+
status={isRunning ? 'busy' : isOnline ? 'online' : undefined}
|
|
163
|
+
heartbeatPulse={heartbeatPulse}
|
|
164
|
+
/>
|
|
133
165
|
<span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{agent.name}</span>
|
|
134
166
|
{pendingApprovalCount > 0 && (
|
|
135
|
-
<span className="shrink-0
|
|
136
|
-
{pendingApprovalCount}
|
|
167
|
+
<span className="shrink-0 text-[9px] font-600 uppercase tracking-wider px-2 py-0.5 rounded-[6px] text-amber-400 bg-amber-400/[0.08] border border-amber-400/15">
|
|
168
|
+
{pendingApprovalCount} {pendingApprovalCount === 1 ? 'approval' : 'approvals'}
|
|
137
169
|
</span>
|
|
138
170
|
)}
|
|
139
171
|
{isDefault && (
|
|
@@ -146,15 +178,16 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
|
|
|
146
178
|
onClick={handleRunClick}
|
|
147
179
|
disabled={running}
|
|
148
180
|
className="shrink-0 text-[10px] font-600 uppercase tracking-wider px-2.5 py-1 rounded-[6px] cursor-pointer
|
|
149
|
-
transition-all border-none bg-
|
|
181
|
+
transition-all border-none bg-accent-bright/20 text-accent-bright hover:bg-accent-bright/30 disabled:opacity-40"
|
|
150
182
|
style={{ fontFamily: 'inherit' }}
|
|
151
183
|
>
|
|
152
184
|
{running ? '...' : 'Run'}
|
|
153
185
|
</button>
|
|
154
186
|
)}
|
|
155
187
|
{agent.isOrchestrator && (
|
|
156
|
-
<span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-amber-400/80 bg-amber-400/[0.08] px-2 py-0.5 rounded-[6px]">
|
|
157
|
-
|
|
188
|
+
<span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-amber-400/80 bg-amber-400/[0.08] px-2 py-0.5 rounded-[6px] flex items-center gap-1">
|
|
189
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M16 3h5v5"/><path d="M21 3l-7 7"/><path d="M8 21H3v-5"/><path d="M3 21l7-7"/></svg>
|
|
190
|
+
delegates
|
|
158
191
|
</span>
|
|
159
192
|
)}
|
|
160
193
|
</div>
|
|
@@ -168,19 +201,19 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
|
|
|
168
201
|
)}
|
|
169
202
|
</div>
|
|
170
203
|
<div className="flex items-center gap-3 mt-1.5 text-[11px] text-text-3/50">
|
|
171
|
-
{
|
|
204
|
+
{agent.lastUsedAt ? (
|
|
172
205
|
<span>Last used: {(() => {
|
|
173
|
-
const days = Math.floor((Date.now() -
|
|
206
|
+
const days = Math.floor((Date.now() - agent.lastUsedAt) / 86400000)
|
|
174
207
|
return days === 0 ? 'today' : `${days}d ago`
|
|
175
208
|
})()}</span>
|
|
176
|
-
) :
|
|
209
|
+
) : agent.updatedAt ? (
|
|
177
210
|
<span>Updated: {(() => {
|
|
178
211
|
const days = Math.floor((Date.now() - agent.updatedAt) / 86400000)
|
|
179
212
|
return days === 0 ? 'today' : `${days}d ago`
|
|
180
213
|
})()}</span>
|
|
181
214
|
) : null}
|
|
182
|
-
{
|
|
183
|
-
<span>Cost: ${
|
|
215
|
+
{agent.totalCost != null && agent.totalCost > 0 && (
|
|
216
|
+
<span>Cost: ${agent.totalCost.toFixed(2)}</span>
|
|
184
217
|
)}
|
|
185
218
|
</div>
|
|
186
219
|
</div>
|
|
@@ -214,7 +247,7 @@ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefaul
|
|
|
214
247
|
<button
|
|
215
248
|
onClick={handleConfirmRun}
|
|
216
249
|
disabled={!taskInput.trim()}
|
|
217
|
-
className="px-4 py-2 rounded-[10px] border-none bg-
|
|
250
|
+
className="px-4 py-2 rounded-[10px] border-none bg-accent-bright text-white text-[13px] font-600 cursor-pointer disabled:opacity-30 transition-all hover:brightness-110"
|
|
218
251
|
style={{ fontFamily: 'inherit' }}
|
|
219
252
|
>
|
|
220
253
|
Run
|