@swarmclawai/swarmclaw 0.6.0 → 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.
Files changed (109) hide show
  1. package/README.md +15 -2
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +3 -2
  15. package/src/app/api/tts/stream/route.ts +3 -2
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +46 -22
  31. package/src/components/chat/chat-header.tsx +455 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +180 -7
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +68 -16
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +51 -11
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/manager.ts +218 -7
  78. package/src/lib/server/heartbeat-service.ts +8 -1
  79. package/src/lib/server/main-agent-loop.ts +1 -1
  80. package/src/lib/server/memory-consolidation.ts +15 -2
  81. package/src/lib/server/memory-db.ts +134 -6
  82. package/src/lib/server/mime.ts +51 -0
  83. package/src/lib/server/openclaw-gateway.ts +2 -2
  84. package/src/lib/server/orchestrator-lg.ts +2 -0
  85. package/src/lib/server/orchestrator.ts +5 -2
  86. package/src/lib/server/playwright-proxy.mjs +2 -3
  87. package/src/lib/server/prompt-runtime-context.ts +53 -0
  88. package/src/lib/server/queue.ts +52 -7
  89. package/src/lib/server/session-tools/canvas.ts +67 -0
  90. package/src/lib/server/session-tools/connector.ts +83 -9
  91. package/src/lib/server/session-tools/crud.ts +21 -0
  92. package/src/lib/server/session-tools/delegate.ts +68 -4
  93. package/src/lib/server/session-tools/git.ts +71 -0
  94. package/src/lib/server/session-tools/http.ts +57 -0
  95. package/src/lib/server/session-tools/index.ts +8 -0
  96. package/src/lib/server/session-tools/memory.ts +1 -0
  97. package/src/lib/server/session-tools/search-providers.ts +16 -8
  98. package/src/lib/server/session-tools/subagent.ts +106 -0
  99. package/src/lib/server/session-tools/web.ts +115 -4
  100. package/src/lib/server/stream-agent-chat.ts +32 -10
  101. package/src/lib/server/task-mention.ts +41 -0
  102. package/src/lib/sessions.ts +10 -0
  103. package/src/lib/soul-library.ts +103 -0
  104. package/src/lib/task-dedupe.ts +26 -0
  105. package/src/lib/tool-definitions.ts +2 -0
  106. package/src/lib/tts.ts +2 -2
  107. package/src/stores/use-app-store.ts +5 -1
  108. package/src/stores/use-chat-store.ts +65 -2
  109. package/src/types/index.ts +32 -2
@@ -0,0 +1,269 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState, useMemo } from 'react'
4
+ import type { Message } from '@/types'
5
+
6
+ /* ─── Heartbeat meta parsing (shared with message-bubble) ─── */
7
+
8
+ interface HeartbeatMeta {
9
+ goal?: string
10
+ status?: string
11
+ next_action?: string
12
+ }
13
+
14
+ function parseHeartbeatMeta(text: string): HeartbeatMeta | null {
15
+ const match = text.match(/\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i)
16
+ if (!match?.[1]) return null
17
+ try {
18
+ const parsed = JSON.parse(match[1])
19
+ if (typeof parsed === 'object' && parsed !== null) return parsed as HeartbeatMeta
20
+ } catch { /* ignore */ }
21
+ return null
22
+ }
23
+
24
+ function heartbeatSummary(text: string): string {
25
+ const clean = (text || '')
26
+ .replace(/\bHEARTBEAT_OK\b/gi, '')
27
+ .replace(/\[AGENT_HEARTBEAT_META\]\s*\{[^\n]*\}/gi, '')
28
+ .replace(/\*\*(.*?)\*\*/g, '$1')
29
+ .replace(/\*(.*?)\*/g, '$1')
30
+ .replace(/`([^`]+)`/g, '$1')
31
+ .replace(/\[(.*?)\]\([^)]+\)/g, '$1')
32
+ .replace(/\bHeartbeat Response\s*:\s*/gi, '')
33
+ .replace(/\bCurrent (State|Status)\s*:\s*/gi, '')
34
+ .replace(/\bRecent Progress\s*:\s*/gi, '')
35
+ .replace(/\bNext (Step|Immediate Step)\s*:\s*/gi, '')
36
+ .replace(/\bStatus\s*:\s*/gi, '')
37
+ .replace(/\s+/g, ' ')
38
+ .trim()
39
+ if (!clean) return 'No new status update.'
40
+ return clean
41
+ }
42
+
43
+ const STATUS_COLORS: Record<string, string> = {
44
+ progress: '#F59E0B',
45
+ ok: '#22C55E',
46
+ idle: '#6B7280',
47
+ blocked: '#EF4444',
48
+ }
49
+
50
+ function relativeTime(ts: number): string {
51
+ const diff = Date.now() - ts
52
+ if (diff < 60_000) return 'just now'
53
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
54
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
55
+ const d = new Date(ts)
56
+ const today = new Date()
57
+ if (d.toDateString() === today.toDateString()) {
58
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
59
+ }
60
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' })
61
+ }
62
+
63
+ function isHeartbeatMessage(msg: Message): boolean {
64
+ return msg.role === 'assistant' && (
65
+ msg.kind === 'heartbeat' ||
66
+ /^\s*HEARTBEAT_OK\b/i.test(msg.text || '') ||
67
+ /^\s*NO_MESSAGE\b/i.test(msg.text || '')
68
+ )
69
+ }
70
+
71
+ function inferStatus(msg: Message, meta: HeartbeatMeta | null): string {
72
+ if (meta?.status) return meta.status.toLowerCase()
73
+ if (msg.suppressed || /^\s*HEARTBEAT_OK\b/i.test(msg.text || '') || /^\s*NO_MESSAGE\b/i.test(msg.text || '')) return 'ok'
74
+ if (msg.toolEvents?.length) return 'progress'
75
+ return 'idle'
76
+ }
77
+
78
+ /* ─── Types ─── */
79
+
80
+ type FilterStatus = 'all' | 'progress' | 'ok' | 'idle' | 'blocked'
81
+
82
+ const FILTER_TABS: { id: FilterStatus; label: string }[] = [
83
+ { id: 'all', label: 'All' },
84
+ { id: 'progress', label: 'Progress' },
85
+ { id: 'ok', label: 'OK' },
86
+ { id: 'idle', label: 'Idle' },
87
+ { id: 'blocked', label: 'Blocked' },
88
+ ]
89
+
90
+ interface HeartbeatEntry {
91
+ msg: Message
92
+ meta: HeartbeatMeta | null
93
+ status: string
94
+ summary: string
95
+ }
96
+
97
+ /* ─── Component ─── */
98
+
99
+ interface Props {
100
+ messages: Message[]
101
+ agentHeartbeatGoal?: string
102
+ onClose: () => void
103
+ }
104
+
105
+ export function HeartbeatHistoryPanel({ messages, agentHeartbeatGoal, onClose }: Props) {
106
+ const [filter, setFilter] = useState<FilterStatus>('all')
107
+ const [expandedIdx, setExpandedIdx] = useState<number | null>(null)
108
+ const scrollRef = useRef<HTMLDivElement>(null)
109
+
110
+ // Build heartbeat entries
111
+ const entries: HeartbeatEntry[] = useMemo(() => {
112
+ return messages
113
+ .filter(isHeartbeatMessage)
114
+ .map((msg) => {
115
+ const meta = parseHeartbeatMeta(msg.text || '')
116
+ const status = inferStatus(msg, meta)
117
+ const summary = heartbeatSummary(msg.text || '')
118
+ return { msg, meta, status, summary }
119
+ })
120
+ }, [messages])
121
+
122
+ // Apply filter
123
+ const filtered = useMemo(() => {
124
+ if (filter === 'all') return entries
125
+ return entries.filter((e) => e.status === filter)
126
+ }, [entries, filter])
127
+
128
+ // Auto-scroll to bottom on open
129
+ useEffect(() => {
130
+ const el = scrollRef.current
131
+ if (el) el.scrollTop = el.scrollHeight
132
+ }, [])
133
+
134
+ // Close on Escape
135
+ useEffect(() => {
136
+ const handler = (e: KeyboardEvent) => {
137
+ if (e.key === 'Escape') onClose()
138
+ }
139
+ window.addEventListener('keydown', handler)
140
+ return () => window.removeEventListener('keydown', handler)
141
+ }, [onClose])
142
+
143
+ return (
144
+ <div className="w-[400px] shrink-0 border-l border-white/[0.06] bg-bg flex flex-col h-full overflow-hidden fade-up-delay">
145
+ {/* Header */}
146
+ <div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.06] shrink-0">
147
+ <div className="flex items-center gap-2">
148
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" className="text-rose-400/70 shrink-0">
149
+ <path d="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" />
150
+ </svg>
151
+ <h3 className="font-display text-[14px] font-600 text-text">Heartbeat History</h3>
152
+ <span className="text-[11px] text-text-3/50 tabular-nums">{entries.length}</span>
153
+ </div>
154
+ <button
155
+ onClick={onClose}
156
+ className="p-1 rounded-[6px] text-text-3/50 hover:text-text-3 bg-transparent border-none cursor-pointer transition-all hover:bg-white/[0.04]"
157
+ aria-label="Close heartbeat history"
158
+ >
159
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
160
+ <line x1="18" y1="6" x2="6" y2="18" />
161
+ <line x1="6" y1="6" x2="18" y2="18" />
162
+ </svg>
163
+ </button>
164
+ </div>
165
+
166
+ {/* Filter tabs */}
167
+ <div className="flex gap-0.5 px-3 pt-2 pb-1 overflow-x-auto shrink-0" role="tablist">
168
+ {FILTER_TABS.map((tab) => (
169
+ <button
170
+ key={tab.id}
171
+ role="tab"
172
+ onClick={() => { setFilter(tab.id); setExpandedIdx(null) }}
173
+ aria-selected={filter === tab.id}
174
+ className={`px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all whitespace-nowrap focus-visible:ring-1 focus-visible:ring-accent-bright/50
175
+ ${filter === tab.id
176
+ ? 'bg-accent-soft text-accent-bright'
177
+ : 'bg-transparent text-text-3 hover:text-text-2'}`}
178
+ style={{ fontFamily: 'inherit' }}
179
+ >
180
+ {tab.label}
181
+ {tab.id !== 'all' && (
182
+ <span className="ml-1 text-[10px] opacity-60">
183
+ {entries.filter((e) => e.status === tab.id).length}
184
+ </span>
185
+ )}
186
+ </button>
187
+ ))}
188
+ </div>
189
+
190
+ {/* Timeline */}
191
+ <div ref={scrollRef} className="flex-1 min-h-0 overflow-y-auto px-3 py-2">
192
+ {filtered.length === 0 ? (
193
+ <div className="flex flex-col items-center justify-center gap-2 py-16 text-center">
194
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.2" strokeLinecap="round" className="text-text-3/30">
195
+ <path d="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" />
196
+ </svg>
197
+ <span className="text-[13px] text-text-3/50">No heartbeat activity yet</span>
198
+ </div>
199
+ ) : (
200
+ <div className="flex flex-col gap-1.5">
201
+ {filtered.map((entry, i) => {
202
+ const color = STATUS_COLORS[entry.status] || STATUS_COLORS.idle
203
+ const goal = entry.meta?.goal || agentHeartbeatGoal
204
+ const isExpanded = expandedIdx === i
205
+ const toolNames = entry.msg.toolEvents?.map((te) => te.name).filter(Boolean) ?? []
206
+ const uniqueTools = [...new Set(toolNames)]
207
+
208
+ return (
209
+ <button
210
+ key={`${entry.msg.time}-${i}`}
211
+ onClick={() => setExpandedIdx(isExpanded ? null : i)}
212
+ className="w-full text-left px-3 py-2.5 rounded-[10px] bg-white/[0.02] border border-white/[0.04] hover:bg-white/[0.04] transition-colors cursor-pointer group"
213
+ style={{ fontFamily: 'inherit' }}
214
+ >
215
+ {/* Top row: status dot + time */}
216
+ <div className="flex items-center gap-2 mb-1">
217
+ <span
218
+ className="w-2 h-2 rounded-full shrink-0"
219
+ style={{ backgroundColor: color }}
220
+ title={entry.status}
221
+ />
222
+ <span className="text-[10px] font-600 uppercase tracking-wider" style={{ color }}>
223
+ {entry.status}
224
+ </span>
225
+ <span className="text-[10px] text-text-3/40 ml-auto tabular-nums">
226
+ {relativeTime(entry.msg.time)}
227
+ </span>
228
+ </div>
229
+
230
+ {/* Goal */}
231
+ {goal && (
232
+ <div className="text-[12px] text-text-2 mb-1 leading-snug line-clamp-2">
233
+ {goal}
234
+ </div>
235
+ )}
236
+
237
+ {/* Next action */}
238
+ {entry.meta?.next_action && (
239
+ <div className="text-[11px] text-text-3/70 mb-1 leading-snug">
240
+ Next: {entry.meta.next_action}
241
+ </div>
242
+ )}
243
+
244
+ {/* Tool chips */}
245
+ {uniqueTools.length > 0 && (
246
+ <div className="flex flex-wrap gap-1 mt-1">
247
+ {uniqueTools.map((name) => (
248
+ <span key={name} className="px-1.5 py-0.5 rounded-[4px] text-[10px] font-600 bg-sky-400/[0.08] text-sky-400/70">
249
+ {name}
250
+ </span>
251
+ ))}
252
+ </div>
253
+ )}
254
+
255
+ {/* Expanded summary */}
256
+ {isExpanded && entry.summary && (
257
+ <div className="mt-2 pt-2 border-t border-white/[0.06] text-[12px] text-text-3 leading-relaxed whitespace-pre-wrap">
258
+ {entry.summary}
259
+ </div>
260
+ )}
261
+ </button>
262
+ )
263
+ })}
264
+ </div>
265
+ )}
266
+ </div>
267
+ </div>
268
+ )
269
+ }