@swarmclawai/swarmclaw 0.5.0 → 0.5.1

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 CHANGED
@@ -81,7 +81,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
81
81
  ```
82
82
 
83
83
  The installer resolves the latest stable release tag and installs that version by default.
84
- To pin a version: `SWARMCLAW_VERSION=v0.5.0 curl ... | bash`
84
+ To pin a version: `SWARMCLAW_VERSION=v0.5.1 curl ... | bash`
85
85
 
86
86
  Or run locally from the repo (friendly for non-technical users):
87
87
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -109,6 +109,34 @@
109
109
  --sidebar-accent-foreground: #e2e2ec;
110
110
  --sidebar-border: rgba(255,255,255,0.04);
111
111
  --sidebar-ring: rgba(99,102,241,0.4);
112
+
113
+ /* ===== Status Badge System ===== */
114
+ --status-idle-bg: rgba(255,255,255,0.04);
115
+ --status-idle-border: rgba(255,255,255,0.06);
116
+ --status-idle-fg: #8e8ea8;
117
+ --status-running-bg: rgba(52,211,153,0.08);
118
+ --status-running-border: rgba(52,211,153,0.15);
119
+ --status-running-fg: #34D399;
120
+ --status-error-bg: rgba(244,63,94,0.08);
121
+ --status-error-border: rgba(244,63,94,0.15);
122
+ --status-error-fg: #F43F5E;
123
+ --status-connecting-bg: rgba(251,191,36,0.08);
124
+ --status-connecting-border: rgba(251,191,36,0.15);
125
+ --status-connecting-fg: #FBBF24;
126
+ --status-connected-bg: rgba(56,189,248,0.08);
127
+ --status-connected-border: rgba(56,189,248,0.15);
128
+ --status-connected-fg: #38BDF8;
129
+ --status-disconnected-bg: var(--status-idle-bg);
130
+ --status-disconnected-border: var(--status-idle-border);
131
+ --status-disconnected-fg: var(--status-idle-fg);
132
+ --status-approval-bg: rgba(251,146,60,0.08);
133
+ --status-approval-border: rgba(251,146,60,0.15);
134
+ --status-approval-fg: #FB923C;
135
+
136
+ /* ===== Command Surface ===== */
137
+ --command-bg: #0a0a14;
138
+ --command-border: rgba(255,255,255,0.06);
139
+ --command-header: rgba(0,0,0,0.3);
112
140
  }
113
141
 
114
142
  @layer base {
@@ -128,10 +156,15 @@ body {
128
156
  }
129
157
 
130
158
  /* Scrollbar */
131
- ::-webkit-scrollbar { width: 4px; }
159
+ * { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.08) transparent; }
160
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
132
161
  ::-webkit-scrollbar-track { background: transparent; }
133
- ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.04); border-radius: 4px; }
134
- ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.1); }
162
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 6px; }
163
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
164
+ textarea { scrollbar-width: none; }
165
+ textarea::-webkit-scrollbar { width: 0; }
166
+ textarea:hover { scrollbar-width: thin; }
167
+ textarea:hover::-webkit-scrollbar { width: 6px; }
135
168
 
136
169
  /* Selection */
137
170
  ::selection { background: rgba(99,102,241,0.3); }
@@ -205,6 +238,10 @@ body {
205
238
  50% { background-position: 100% 50%; }
206
239
  100% { background-position: 0% 50%; }
207
240
  }
241
+ @keyframes fadeUp {
242
+ from { opacity: 0; transform: translateY(10px); }
243
+ to { opacity: 1; transform: translateY(0); }
244
+ }
208
245
 
209
246
  /* AI avatar mood animations */
210
247
  @keyframes ai-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.15); } }
@@ -377,6 +414,44 @@ body {
377
414
  font-family: var(--font-sora), 'Sora', system-ui, sans-serif;
378
415
  }
379
416
 
417
+ /* ===== Status Badge Classes ===== */
418
+ .status-badge-idle, .status-badge-running, .status-badge-error,
419
+ .status-badge-connecting, .status-badge-connected, .status-badge-disconnected,
420
+ .status-badge-approval {
421
+ display: inline-flex; align-items: center; gap: 0.375rem;
422
+ padding: 0.125rem 0.5rem; border-radius: 6px;
423
+ font-size: 10px; font-weight: 600; text-transform: uppercase;
424
+ letter-spacing: 0.05em; border: 1px solid;
425
+ }
426
+ .status-badge-idle { background: var(--status-idle-bg); border-color: var(--status-idle-border); color: var(--status-idle-fg); }
427
+ .status-badge-running { background: var(--status-running-bg); border-color: var(--status-running-border); color: var(--status-running-fg); }
428
+ .status-badge-error { background: var(--status-error-bg); border-color: var(--status-error-border); color: var(--status-error-fg); }
429
+ .status-badge-connecting { background: var(--status-connecting-bg); border-color: var(--status-connecting-border); color: var(--status-connecting-fg); }
430
+ .status-badge-connected { background: var(--status-connected-bg); border-color: var(--status-connected-border); color: var(--status-connected-fg); }
431
+ .status-badge-disconnected { background: var(--status-disconnected-bg); border-color: var(--status-disconnected-border); color: var(--status-disconnected-fg); }
432
+ .status-badge-approval { background: var(--status-approval-bg); border-color: var(--status-approval-border); color: var(--status-approval-fg); }
433
+
434
+ /* ===== Fade-Up Animation ===== */
435
+ .fade-up { animation: fadeUp 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; }
436
+ .fade-up-delay { animation: fadeUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) 0.08s both; }
437
+
438
+ /* ===== Card Select Indicator ===== */
439
+ .card-select-indicator {
440
+ position: absolute;
441
+ left: 0; top: 0.7rem; bottom: 0.7rem;
442
+ width: 4px;
443
+ border-radius: 0 9999px 9999px 0;
444
+ background: linear-gradient(180deg, #818CF8 0%, #6366F1 50%, #4F46E5 100%);
445
+ }
446
+
447
+ /* ===== Command Surface ===== */
448
+ .command-surface {
449
+ background: var(--command-bg);
450
+ border: 1px solid var(--command-border);
451
+ border-radius: 12px;
452
+ overflow: hidden;
453
+ }
454
+
380
455
  @layer base {
381
456
  * {
382
457
  @apply border-border outline-ring/50;
@@ -22,10 +22,11 @@ interface Props {
22
22
  agent: Agent
23
23
  isDefault?: boolean
24
24
  isRunning?: boolean
25
+ isSelected?: boolean
25
26
  onSetDefault?: (id: string) => void
26
27
  }
27
28
 
28
- export function AgentCard({ agent, isDefault, isRunning, onSetDefault }: Props) {
29
+ export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefault }: Props) {
29
30
  const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
30
31
  const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
31
32
  const loadSessions = useAppStore((s) => s.loadSessions)
@@ -85,10 +86,13 @@ export function AgentCard({ agent, isDefault, isRunning, onSetDefault }: Props)
85
86
  <>
86
87
  <div
87
88
  onClick={handleClick}
88
- className="group relative py-3.5 px-4 cursor-pointer rounded-[14px]
89
+ className={`group relative py-3.5 px-4 cursor-pointer rounded-[14px]
89
90
  transition-all duration-200 active:scale-[0.98]
90
- bg-transparent border border-transparent hover:bg-white/[0.05] hover:border-white/[0.08]"
91
+ ${isSelected
92
+ ? 'bg-white/[0.04] border border-white/[0.08]'
93
+ : 'bg-transparent border border-transparent hover:bg-white/[0.05] hover:border-white/[0.08]'}`}
91
94
  >
95
+ {isSelected && <div className="card-select-indicator" />}
92
96
  {/* Three-dot dropdown */}
93
97
  <DropdownMenu>
94
98
  <DropdownMenuTrigger asChild>
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useMemo, useState, useCallback } from 'react'
3
+ import { useEffect, useLayoutEffect, useMemo, useRef, useState, useCallback } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
6
  import { AgentCard } from './agent-card'
@@ -23,10 +23,18 @@ export function AgentList({ inSidebar }: Props) {
23
23
  const setShowTrash = useAppStore((s) => s.setShowTrash)
24
24
  const fleetFilter = useAppStore((s) => s.fleetFilter)
25
25
  const setFleetFilter = useAppStore((s) => s.setFleetFilter)
26
+ const currentSessionId = useAppStore((s) => s.currentSessionId)
26
27
  const approvals = useApprovalStore((s) => s.approvals)
27
28
  const [search, setSearch] = useState('')
28
29
  const [filter, setFilter] = useState<'all' | 'orchestrator' | 'agent'>('all')
29
30
 
31
+ // FLIP animation refs
32
+ const flipPositions = useRef<Map<string, number>>(new Map())
33
+ const cardRefs = useRef<Map<string, HTMLDivElement>>(new Map())
34
+
35
+ const currentSession = currentSessionId ? sessions[currentSessionId] : null
36
+ const selectedAgentId = currentSession?.agentId
37
+
30
38
  const mainSession = useMemo(() =>
31
39
  Object.values(sessions).find((s: any) => s.name === '__main__' && s.user === currentUser),
32
40
  [sessions, currentUser]
@@ -76,6 +84,26 @@ export function AgentList({ inSidebar }: Props) {
76
84
  .sort((a, b) => b.updatedAt - a.updatedAt)
77
85
  }, [agents, search, filter, activeProjectFilter, fleetFilter, runningAgentIds, approvalsByAgent])
78
86
 
87
+ // FLIP animation: animate agent cards when order changes
88
+ useLayoutEffect(() => {
89
+ const newPositions = new Map<string, number>()
90
+ for (const [id, el] of cardRefs.current) {
91
+ const newTop = el.getBoundingClientRect().top
92
+ newPositions.set(id, newTop)
93
+ const prevTop = flipPositions.current.get(id)
94
+ if (prevTop != null) {
95
+ const delta = prevTop - newTop
96
+ if (Math.abs(delta) > 1) {
97
+ el.animate(
98
+ [{ transform: `translateY(${delta}px)` }, { transform: 'translateY(0)' }],
99
+ { duration: 300, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' }
100
+ )
101
+ }
102
+ }
103
+ }
104
+ flipPositions.current = newPositions
105
+ }, [filtered])
106
+
79
107
  if (showTrash) {
80
108
  return (
81
109
  <div className="flex-1 flex flex-col overflow-hidden">
@@ -124,7 +152,7 @@ export function AgentList({ inSidebar }: Props) {
124
152
  }
125
153
 
126
154
  return (
127
- <div className="flex-1 overflow-y-auto">
155
+ <div className="flex-1 overflow-y-auto fade-up">
128
156
  {(filtered.length > 3 || search) && (
129
157
  <div className="px-4 py-2.5">
130
158
  <input
@@ -183,7 +211,9 @@ export function AgentList({ inSidebar }: Props) {
183
211
  </div>
184
212
  <div className="flex flex-col gap-1 px-2 pb-4">
185
213
  {filtered.map((p) => (
186
- <AgentCard key={p.id} agent={p} isDefault={p.id === defaultAgentId} isRunning={runningAgentIds.has(p.id)} onSetDefault={handleSetDefault} />
214
+ <div key={p.id} ref={(el) => { if (el) cardRefs.current.set(p.id, el); else cardRefs.current.delete(p.id) }}>
215
+ <AgentCard agent={p} isDefault={p.id === defaultAgentId} isRunning={runningAgentIds.has(p.id)} isSelected={p.id === selectedAgentId} onSetDefault={handleSetDefault} />
216
+ </div>
187
217
  ))}
188
218
  </div>
189
219
  </div>
@@ -52,7 +52,7 @@ export function InspectorPanel({ agent }: Props) {
52
52
  const agentSchedules = Object.values(schedules).filter((s) => s.agentId === agent.id)
53
53
 
54
54
  return (
55
- <div className="w-[400px] shrink-0 border-l border-white/[0.06] bg-[#0d0f1a] flex flex-col h-full overflow-hidden">
55
+ <div className="w-[400px] shrink-0 border-l border-white/[0.06] bg-[#0d0f1a] flex flex-col h-full overflow-hidden fade-up-delay">
56
56
  {/* Header */}
57
57
  <div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.06] shrink-0">
58
58
  <h3 className="font-display text-[14px] font-600 text-text truncate">{agent.name}</h3>
@@ -56,7 +56,7 @@ export function CodeBlock({ children, className }: Props) {
56
56
  }, [getText, language])
57
57
 
58
58
  return (
59
- <div className="relative group/code">
59
+ <div className="relative group/code command-surface">
60
60
  <div className="flex items-center justify-between px-4 py-2 bg-black/30 border-b border-white/[0.03]">
61
61
  <span className="text-[10px] font-600 uppercase tracking-[0.08em] text-text-3 font-mono">{language}</span>
62
62
  <div className="flex items-center gap-1">
@@ -376,6 +376,15 @@ interface Props {
376
376
  onFork?: (index: number) => void
377
377
  }
378
378
 
379
+ function isStructuredMarkdown(text: string): boolean {
380
+ if (!text) return false
381
+ return /```/.test(text)
382
+ || /^#{1,4}\s/m.test(text)
383
+ || /^[-*]\s/m.test(text)
384
+ || /^\d+\.\s/m.test(text)
385
+ || /\|.*\|.*\|/m.test(text)
386
+ }
387
+
379
388
  export const MessageBubble = memo(function MessageBubble({ message, assistantName, agentAvatarSeed, agentName, isLast, onRetry, messageIndex, onToggleBookmark, onEditResend, onFork }: Props) {
380
389
  const isUser = message.role === 'user'
381
390
  const isHeartbeat = !isUser && (message.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(message.text || ''))
@@ -388,6 +397,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
388
397
  const toolEvents = message.toolEvents || []
389
398
  const hasToolEvents = !isUser && toolEvents.length > 0
390
399
  const visibleToolEvents = toolEventsExpanded ? [...toolEvents].reverse() : toolEvents.slice(-1)
400
+ const isStructured = !isUser && !isHeartbeat && isStructuredMarkdown(message.text)
391
401
 
392
402
  const handleCopy = useCallback(() => {
393
403
  navigator.clipboard.writeText(message.text).then(() => {
@@ -442,7 +452,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
442
452
  )}
443
453
 
444
454
  {/* Message bubble */}
445
- <div className={`max-w-[85%] md:max-w-[72%] ${isUser ? 'bubble-user px-5 py-3.5' : isHeartbeat ? 'bubble-ai px-4 py-3' : 'bubble-ai px-5 py-3.5'}`}>
455
+ <div className={`${isStructured ? 'max-w-[92%] md:max-w-[85%]' : 'max-w-[85%] md:max-w-[72%]'} ${isUser ? 'bubble-user px-5 py-3.5' : isHeartbeat ? 'bubble-ai px-4 py-3' : 'bubble-ai px-5 py-3.5'}`}>
446
456
  {renderAttachments(message)}
447
457
 
448
458
  {isHeartbeat ? (
@@ -5,6 +5,7 @@ import type { Message } from '@/types'
5
5
  import { useChatStore } from '@/stores/use-chat-store'
6
6
  import { useAppStore } from '@/stores/use-app-store'
7
7
  import { api } from '@/lib/api-client'
8
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
8
9
  import { MessageBubble } from './message-bubble'
9
10
  import { StreamingBubble } from './streaming-bubble'
10
11
  import { ThinkingIndicator } from './thinking-indicator'
@@ -12,6 +13,23 @@ import { SuggestionsBar } from './suggestions-bar'
12
13
  import { ExecApprovalCard } from './exec-approval-card'
13
14
  import { useApprovalStore } from '@/stores/use-approval-store'
14
15
 
16
+ const INTRO_GREETINGS = [
17
+ 'What can I help you with?',
18
+ 'Ready when you are.',
19
+ "Let's get started.",
20
+ 'How can I assist you today?',
21
+ 'What are we working on?',
22
+ ]
23
+
24
+ function stableHash(str: string): number {
25
+ let hash = 0
26
+ for (let i = 0; i < str.length; i++) {
27
+ hash = ((hash << 5) - hash) + str.charCodeAt(i)
28
+ hash |= 0
29
+ }
30
+ return Math.abs(hash)
31
+ }
32
+
15
33
  function dateSeparator(ts: number): string {
16
34
  const d = new Date(ts)
17
35
  const today = new Date()
@@ -316,9 +334,18 @@ export function MessageList({ messages, streaming }: Props) {
316
334
  <div
317
335
  ref={scrollRef}
318
336
  onScroll={updateScrollState}
319
- className="h-full overflow-y-auto px-6 md:px-12 lg:px-16 py-6"
337
+ className="h-full overflow-y-auto px-6 md:px-12 lg:px-16 py-6 fade-up"
320
338
  >
321
339
  <div className="flex flex-col gap-6">
340
+ {filteredMessages.length === 0 && !streaming && (
341
+ <div className="flex flex-col items-center justify-center gap-3 py-20 text-center" style={{ animation: 'fadeUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) both' }}>
342
+ <AgentAvatar seed={agent?.avatarSeed || null} name={agent?.name || 'Agent'} size={48} />
343
+ <span className="font-display text-[16px] font-600 text-text-2">{agent?.name || 'Assistant'}</span>
344
+ <span className="text-[14px] text-text-3/60">
345
+ {INTRO_GREETINGS[stableHash(agent?.id || session?.id || '') % INTRO_GREETINGS.length]}
346
+ </span>
347
+ </div>
348
+ )}
322
349
  {filteredMessages.map((msg, i) => {
323
350
  // Find original index in the full messages array for API calls
324
351
  const originalIndex = messages.indexOf(msg)
@@ -1,6 +1,5 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
4
3
  import type { ChatTraceBlock } from '@/types'
5
4
 
6
5
  interface Props {
@@ -8,8 +7,6 @@ interface Props {
8
7
  }
9
8
 
10
9
  export function TraceBlock({ trace }: Props) {
11
- const [collapsed, setCollapsed] = useState(trace.collapsed !== false)
12
-
13
10
  const bgColor = trace.type === 'thinking'
14
11
  ? 'bg-purple-500/[0.04] border-purple-500/10'
15
12
  : trace.type === 'tool-call'
@@ -29,32 +26,25 @@ export function TraceBlock({ trace }: Props) {
29
26
  : '<'
30
27
 
31
28
  return (
32
- <div className={`my-1 rounded-[8px] border ${bgColor} overflow-hidden`}>
33
- <button
34
- onClick={() => setCollapsed(!collapsed)}
35
- className={`w-full flex items-center gap-2 px-3 py-1.5 text-left cursor-pointer border-none bg-transparent transition-colors hover:bg-white/[0.02] ${labelColor}`}
36
- style={{ fontFamily: 'inherit' }}
37
- >
38
- <span className="font-mono text-[10px] w-4 shrink-0">{collapsed ? '+' : '-'}</span>
29
+ <details className={`my-1 rounded-[8px] border ${bgColor} overflow-hidden`} open={trace.collapsed === false || undefined}>
30
+ <summary className={`flex items-center gap-2 px-3 py-1.5 cursor-pointer select-none transition-colors hover:bg-white/[0.02] ${labelColor} [&::-webkit-details-marker]:hidden list-none`}>
39
31
  <span className="font-mono text-[10px] shrink-0">{icon}</span>
40
32
  <span className="text-[11px] font-600 truncate">
41
33
  {trace.label || trace.type.replace('-', ' ')}
42
34
  </span>
43
- </button>
44
- {!collapsed && (
45
- <div className="px-3 pb-2">
46
- <pre className={`text-[11px] leading-relaxed whitespace-pre-wrap break-words m-0 ${
47
- trace.type === 'thinking'
48
- ? 'text-text-3/60 italic'
49
- : 'text-text-3/70 font-mono'
50
- }`}>
51
- {trace.content.length > 2000
52
- ? trace.content.slice(0, 2000) + '\n... (truncated)'
53
- : trace.content}
54
- </pre>
55
- </div>
56
- )}
57
- </div>
35
+ </summary>
36
+ <div className="px-3 pb-2">
37
+ <pre className={`text-[11px] leading-relaxed whitespace-pre-wrap break-words m-0 ${
38
+ trace.type === 'thinking'
39
+ ? 'text-text-3/60 italic'
40
+ : 'text-text-3/70 font-mono'
41
+ }`}>
42
+ {trace.content.length > 2000
43
+ ? trace.content.slice(0, 2000) + '\n... (truncated)'
44
+ : trace.content}
45
+ </pre>
46
+ </div>
47
+ </details>
58
48
  )
59
49
  }
60
50