@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.
- package/README.md +15 -2
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- 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/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -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/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- 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/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +455 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +180 -7
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +68 -16
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- 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-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -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 +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- 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 +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- 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/queue.ts +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- 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 +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- 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/stream-agent-chat.ts +32 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- 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
|
+
}
|