@swarmclawai/swarmclaw 0.6.0 → 0.6.3

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 (118) hide show
  1. package/README.md +56 -42
  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 +16 -35
  15. package/src/app/api/tts/stream/route.ts +14 -42
  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 +76 -24
  31. package/src/components/chat/chat-header.tsx +522 -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 +113 -8
  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 +84 -17
  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 +125 -14
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/connector-routing.test.ts +118 -1
  78. package/src/lib/server/connectors/discord.ts +31 -8
  79. package/src/lib/server/connectors/manager.ts +594 -16
  80. package/src/lib/server/connectors/media.ts +5 -0
  81. package/src/lib/server/connectors/telegram.ts +12 -2
  82. package/src/lib/server/connectors/types.ts +2 -0
  83. package/src/lib/server/connectors/whatsapp.ts +28 -2
  84. package/src/lib/server/elevenlabs.test.ts +60 -0
  85. package/src/lib/server/elevenlabs.ts +103 -0
  86. package/src/lib/server/heartbeat-service.ts +8 -1
  87. package/src/lib/server/main-agent-loop.ts +1 -1
  88. package/src/lib/server/memory-consolidation.ts +15 -2
  89. package/src/lib/server/memory-db.ts +134 -6
  90. package/src/lib/server/mime.ts +51 -0
  91. package/src/lib/server/openclaw-gateway.ts +2 -2
  92. package/src/lib/server/orchestrator-lg.ts +2 -0
  93. package/src/lib/server/orchestrator.ts +5 -2
  94. package/src/lib/server/playwright-proxy.mjs +2 -3
  95. package/src/lib/server/prompt-runtime-context.ts +53 -0
  96. package/src/lib/server/queue.ts +182 -8
  97. package/src/lib/server/session-tools/canvas.ts +67 -0
  98. package/src/lib/server/session-tools/connector.ts +583 -63
  99. package/src/lib/server/session-tools/crud.ts +21 -0
  100. package/src/lib/server/session-tools/delegate.ts +68 -4
  101. package/src/lib/server/session-tools/file.ts +26 -7
  102. package/src/lib/server/session-tools/git.ts +71 -0
  103. package/src/lib/server/session-tools/http.ts +57 -0
  104. package/src/lib/server/session-tools/index.ts +8 -0
  105. package/src/lib/server/session-tools/memory.ts +1 -0
  106. package/src/lib/server/session-tools/search-providers.ts +16 -8
  107. package/src/lib/server/session-tools/subagent.ts +106 -0
  108. package/src/lib/server/session-tools/web.ts +118 -8
  109. package/src/lib/server/stream-agent-chat.ts +39 -10
  110. package/src/lib/server/task-mention.ts +41 -0
  111. package/src/lib/sessions.ts +10 -0
  112. package/src/lib/soul-library.ts +103 -0
  113. package/src/lib/task-dedupe.ts +26 -0
  114. package/src/lib/tool-definitions.ts +2 -0
  115. package/src/lib/tts.ts +2 -2
  116. package/src/stores/use-app-store.ts +5 -1
  117. package/src/stores/use-chat-store.ts +65 -2
  118. package/src/types/index.ts +32 -2
@@ -0,0 +1,96 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useCallback } from 'react'
4
+ import { useWs } from '@/hooks/use-ws'
5
+ import { api } from '@/lib/api-client'
6
+
7
+ interface CanvasPanelProps {
8
+ sessionId: string
9
+ agentName?: string
10
+ onClose: () => void
11
+ }
12
+
13
+ export function CanvasPanel({ sessionId, agentName, onClose }: CanvasPanelProps) {
14
+ const [content, setContent] = useState<string | null>(null)
15
+
16
+ const loadCanvas = useCallback(async () => {
17
+ try {
18
+ const res = await api<{ content: string | null }>('GET', `/canvas/${sessionId}`)
19
+ setContent(res.content)
20
+ } catch { /* ignore */ }
21
+ }, [sessionId])
22
+
23
+ useEffect(() => { loadCanvas() }, [loadCanvas]) // eslint-disable-line react-hooks/set-state-in-effect
24
+ useWs(`canvas:${sessionId}`, loadCanvas, 10_000)
25
+
26
+ if (!content) return (
27
+ <div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
28
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
29
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
30
+ <rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
31
+ </svg>
32
+ <span className="text-[13px] font-600 text-text flex-1 truncate">
33
+ Canvas{agentName ? ` — ${agentName}` : ''}
34
+ </span>
35
+ <button
36
+ onClick={onClose}
37
+ className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
38
+ title="Close canvas"
39
+ aria-label="Close canvas"
40
+ >
41
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
42
+ <path d="M18 6L6 18M6 6l12 12" />
43
+ </svg>
44
+ </button>
45
+ </div>
46
+ <div className="flex-1 flex items-center justify-center">
47
+ <div className="text-center">
48
+ <div className="w-8 h-8 rounded-full border-2 border-text-3/20 border-t-accent-bright animate-spin mx-auto mb-3" />
49
+ <span className="text-[13px] text-text-3">Loading canvas...</span>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ )
54
+
55
+ return (
56
+ <div className="flex flex-col h-full border-l border-white/[0.06] bg-bg min-w-[400px]">
57
+ {/* Toolbar */}
58
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06] shrink-0">
59
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright shrink-0">
60
+ <rect x="2" y="3" width="20" height="14" rx="2" /><path d="M8 21h8" /><path d="M12 17v4" />
61
+ </svg>
62
+ <span className="text-[13px] font-600 text-text flex-1 truncate">
63
+ Canvas{agentName ? ` — ${agentName}` : ''}
64
+ </span>
65
+ <button
66
+ onClick={loadCanvas}
67
+ className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
68
+ title="Refresh"
69
+ >
70
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
71
+ <polyline points="23 4 23 10 17 10" /><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
72
+ </svg>
73
+ </button>
74
+ <button
75
+ onClick={onClose}
76
+ className="p-1.5 rounded-[6px] hover:bg-white/[0.06] transition-colors cursor-pointer border-none bg-transparent text-text-3 hover:text-text-2"
77
+ title="Close canvas"
78
+ >
79
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
80
+ <path d="M18 6L6 18M6 6l12 12" />
81
+ </svg>
82
+ </button>
83
+ </div>
84
+
85
+ {/* Sandboxed iframe */}
86
+ <div className="flex-1 overflow-hidden">
87
+ <iframe
88
+ sandbox="allow-scripts allow-same-origin"
89
+ srcDoc={content}
90
+ className="w-full h-full border-none bg-white"
91
+ title="Agent Canvas"
92
+ />
93
+ </div>
94
+ </div>
95
+ )
96
+ }
@@ -11,6 +11,8 @@ const NOTABLE_TOOLS: Record<string, { label: string; color: string; icon: 'brain
11
11
  delegate_to_claude_code: { label: 'Delegated to Claude Code', color: '#38BDF8', icon: 'delegate' },
12
12
  delegate_to_codex_cli: { label: 'Delegated to Codex', color: '#38BDF8', icon: 'delegate' },
13
13
  delegate_to_opencode_cli: { label: 'Delegated to OpenCode', color: '#38BDF8', icon: 'delegate' },
14
+ delegate_to_agent: { label: 'Delegating task', color: '#6366F1', icon: 'delegate' },
15
+ check_delegation_status: { label: 'Checking delegation', color: '#6366F1', icon: 'delegate' },
14
16
  web_search: { label: 'Searched the web', color: '#22C55E', icon: 'search' },
15
17
  connector_message_tool: { label: 'Sent a message', color: '#F97316', icon: 'message' },
16
18
  }
@@ -23,6 +25,8 @@ function extractSnippet(toolName: string, toolInput: string): string | null {
23
25
  if (toolName === 'manage_tasks' && parsed.title) return parsed.title
24
26
  if (toolName === 'manage_schedules' && parsed.name) return parsed.name
25
27
  if (toolName === 'manage_agents' && parsed.name) return parsed.name
28
+ if (toolName === 'delegate_to_agent' && (parsed.agentName || parsed.agentId)) return parsed.agentName || parsed.agentId
29
+ if (toolName === 'check_delegation_status' && parsed.agentName) return parsed.agentName
26
30
  if (toolName.startsWith('delegate_to_') && parsed.task) return parsed.task
27
31
  if (toolName === 'web_search' && parsed.query) return parsed.query
28
32
  if (toolName === 'connector_message_tool' && parsed.to) return parsed.to
@@ -104,8 +108,8 @@ export function ActivityMoment({ toolName, toolInput, onDismiss }: Props) {
104
108
  <div
105
109
  className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] shadow-lg whitespace-nowrap"
106
110
  style={{
107
- background: `${config.color}18`,
108
- border: `1px solid ${config.color}30`,
111
+ background: 'var(--card)',
112
+ border: `1px solid ${config.color}40`,
109
113
  }}
110
114
  >
111
115
  <MomentIcon icon={config.icon} color={config.color} />
@@ -153,8 +157,8 @@ export function HeartbeatMoment({ onDismiss }: { onDismiss: () => void }) {
153
157
  <div
154
158
  className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] shadow-lg whitespace-nowrap"
155
159
  style={{
156
- background: 'rgba(34,197,94,0.1)',
157
- border: '1px solid rgba(34,197,94,0.2)',
160
+ background: 'var(--card)',
161
+ border: '1px solid rgba(34,197,94,0.3)',
158
162
  }}
159
163
  >
160
164
  <svg width="11" height="11" viewBox="0 0 24 24" fill="#22c55e">
@@ -1,10 +1,10 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useCallback, useState, useRef } from 'react'
3
+ import { useEffect, useCallback, useState, useRef, useMemo } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { useWs } from '@/hooks/use-ws'
6
6
  import { useChatStore } from '@/stores/use-chat-store'
7
- import { fetchMessages, clearMessages, deleteSession, devServer, checkBrowser, stopBrowser } from '@/lib/sessions'
7
+ import { fetchMessages, fetchMessagesPaginated, clearMessages, deleteSession, devServer, checkBrowser, stopBrowser } from '@/lib/sessions'
8
8
  import { uploadImage } from '@/lib/upload'
9
9
  import { deleteAgent } from '@/lib/agents'
10
10
  import { useMediaQuery } from '@/hooks/use-media-query'
@@ -17,6 +17,7 @@ import { useVoiceConversation } from '@/hooks/use-voice-conversation'
17
17
  import { ChatInput } from '@/components/input/chat-input'
18
18
  import { ChatPreviewPanel } from './chat-preview-panel'
19
19
  import { InspectorPanel } from '@/components/agents/inspector-panel'
20
+ import { HeartbeatHistoryPanel } from './heartbeat-history-panel'
20
21
  import { Dropdown, DropdownItem } from '@/components/shared/dropdown'
21
22
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
22
23
  import { speak } from '@/lib/tts'
@@ -47,6 +48,8 @@ export function ChatArea() {
47
48
  const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
48
49
  const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
49
50
  const inspectorOpen = useAppStore((s) => s.inspectorOpen)
51
+ const sidebarOpen = useAppStore((s) => s.sidebarOpen)
52
+ const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
50
53
  const currentAgent = session?.agentId ? agents[session.agentId] ?? null : null
51
54
 
52
55
  const voice = useVoiceConversation()
@@ -60,6 +63,28 @@ export function ChatArea() {
60
63
  const [confirmClear, setConfirmClear] = useState(false)
61
64
  const [confirmDeleteAgent, setConfirmDeleteAgent] = useState(false)
62
65
  const [browserActive, setBrowserActive] = useState(false)
66
+ const [heartbeatHistoryOpen, setHeartbeatHistoryOpen] = useState(false)
67
+ const [messagesLoading, setMessagesLoading] = useState(true)
68
+ const [connectorFilter, setConnectorFilter] = useState<string | null>(null)
69
+
70
+ // Collect unique connector sources from messages for filter UI
71
+ const { connectorSources, hasDirectMessages } = useMemo(() => {
72
+ const sources = new Map<string, { platform: string; connectorName: string }>()
73
+ let hasDirect = false
74
+ for (const msg of messages) {
75
+ if (msg.source?.connectorId && !sources.has(msg.source.connectorId)) {
76
+ sources.set(msg.source.connectorId, {
77
+ platform: msg.source.platform,
78
+ connectorName: msg.source.connectorName,
79
+ })
80
+ } else if (!msg.source?.connectorId && msg.role === 'user') {
81
+ hasDirect = true
82
+ }
83
+ }
84
+ return { connectorSources: sources, hasDirectMessages: hasDirect }
85
+ }, [messages])
86
+ // Show source filter when there are genuinely multiple sources (2+ connectors, or connector + direct)
87
+ const hasMultipleSources = connectorSources.size > 1 || (connectorSources.size > 0 && hasDirectMessages)
63
88
  const [isDragging, setIsDragging] = useState(false)
64
89
  const dragCounter = useRef(0)
65
90
  const setPendingImage = useChatStore((s) => s.setPendingImage)
@@ -69,13 +94,19 @@ export function ChatArea() {
69
94
  const chatState = useChatStore.getState()
70
95
  const preserveLocalStream = chatState.streaming && chatState.streamingSessionId === sessionId
71
96
  // Clear stale state from the previous session, but keep active local stream state for this session.
97
+ setMessagesLoading(true)
72
98
  setMessages([])
73
99
  if (!preserveLocalStream) {
74
100
  useChatStore.setState({ streaming: false, streamingSessionId: null, streamText: '', toolEvents: [] })
75
101
  }
76
- fetchMessages(sessionId).then(setMessages).catch((err) => {
102
+ fetchMessagesPaginated(sessionId, 100).then((data) => {
103
+ setMessages(data.messages)
104
+ useChatStore.setState({ hasMoreMessages: data.hasMore, totalMessages: data.total })
105
+ }).catch((err) => {
77
106
  console.error('Failed to load messages:', err)
78
107
  setMessages(session?.messages || [])
108
+ }).finally(() => {
109
+ setMessagesLoading(false)
79
110
  })
80
111
  // If server reports session is still active, show streaming state
81
112
  if (session?.active) {
@@ -125,12 +156,13 @@ export function ChatArea() {
125
156
  return !isHeartbeat && !!m.text?.trim()
126
157
  })
127
158
  if (latestAssistant?.text) {
128
- void speak(latestAssistant.text)
159
+ void speak(latestAssistant.text, currentAgent?.elevenLabsVoiceId)
129
160
  }
130
161
  }
131
162
  }
132
163
  if (isServerActiveRef.current) await loadSessions()
133
164
  } catch (err) { console.error('Failed to refresh messages:', err) }
165
+ // eslint-disable-next-line react-hooks/exhaustive-deps
134
166
  }, [sessionId])
135
167
 
136
168
  // Subscribe to WS messages for this session — always subscribe when session exists,
@@ -198,10 +230,6 @@ export function ChatArea() {
198
230
  setCurrentSession(null)
199
231
  }, [sessionId])
200
232
 
201
- const handleBack = useCallback(() => {
202
- setCurrentSession(null)
203
- }, [])
204
-
205
233
  const handlePrompt = useCallback((text: string) => {
206
234
  sendMessage(text)
207
235
  }, [sendMessage])
@@ -244,7 +272,7 @@ export function ChatArea() {
244
272
 
245
273
  const streamingForThisSession = streaming && (!streamingSessionId || streamingSessionId === session.id)
246
274
  const isMainChat = session.name === '__main__'
247
- const isEmpty = !messages.length && !streamingForThisSession
275
+ const isEmpty = !messages.length && !streamingForThisSession && !messagesLoading
248
276
 
249
277
  return (
250
278
  <div className="flex-1 flex h-full min-h-0 min-w-0">
@@ -261,12 +289,18 @@ export function ChatArea() {
261
289
  streaming={streamingForThisSession}
262
290
  onStop={stopStreaming}
263
291
  onMenuToggle={() => setMenuOpen(!menuOpen)}
264
- onBack={handleBack}
292
+ onBack={sidebarOpen ? () => setSidebarOpen(false) : undefined}
265
293
  browserActive={browserActive}
266
294
  onStopBrowser={handleStopBrowser}
267
295
  voiceActive={voice.active}
268
296
  voiceSupported={voice.supported}
269
297
  onVoiceToggle={handleVoiceToggle}
298
+ heartbeatHistoryOpen={heartbeatHistoryOpen}
299
+ onToggleHeartbeatHistory={() => setHeartbeatHistoryOpen((v) => !v)}
300
+ connectorSources={connectorSources}
301
+ connectorFilter={connectorFilter}
302
+ onConnectorFilterChange={setConnectorFilter}
303
+ hasMultipleSources={hasMultipleSources}
270
304
  />
271
305
  )}
272
306
  {!isDesktop && (
@@ -281,11 +315,25 @@ export function ChatArea() {
281
315
  voiceActive={voice.active}
282
316
  voiceSupported={voice.supported}
283
317
  onVoiceToggle={handleVoiceToggle}
318
+ connectorSources={connectorSources}
319
+ connectorFilter={connectorFilter}
320
+ onConnectorFilterChange={setConnectorFilter}
321
+ hasMultipleSources={hasMultipleSources}
284
322
  />
285
323
  )}
286
324
  <DevServerBar status={devServerStatus} onStop={handleStopDevServer} />
287
325
 
288
- {isEmpty ? (
326
+ {messagesLoading && !messages.length ? (
327
+ <div className="flex-1 flex items-center justify-center">
328
+ <div className="flex flex-col items-center gap-3" style={{ animation: 'fade-in 0.2s ease' }}>
329
+ <div className="relative w-10 h-10">
330
+ <div className="absolute inset-0 rounded-full border-2 border-white/[0.06]" />
331
+ <div className="absolute inset-0 rounded-full border-2 border-transparent border-t-accent-bright animate-spin" />
332
+ </div>
333
+ <span className="text-[13px] text-text-3/50 font-500">Loading messages...</span>
334
+ </div>
335
+ </div>
336
+ ) : isEmpty ? (
289
337
  <div className="flex-1 flex flex-col items-center justify-center px-6 pb-4 relative">
290
338
  {/* Atmospheric background glow */}
291
339
  <div className="absolute inset-0 pointer-events-none overflow-hidden">
@@ -336,7 +384,7 @@ export function ChatArea() {
336
384
  </div>
337
385
  </div>
338
386
  ) : (
339
- <MessageList messages={messages} streaming={streamingForThisSession} />
387
+ <MessageList messages={messages} streaming={streamingForThisSession} connectorFilter={connectorFilter} />
340
388
  )}
341
389
 
342
390
  {voice.active && (
@@ -361,19 +409,9 @@ export function ChatArea() {
361
409
  />
362
410
 
363
411
  <Dropdown open={menuOpen} onClose={() => setMenuOpen(false)}>
364
- {session.agentId && agents[session.agentId] && (
365
- <DropdownItem onClick={() => { setMenuOpen(false); setEditingAgentId(session.agentId!); setAgentSheetOpen(true) }}>
366
- Edit Agent
367
- </DropdownItem>
368
- )}
369
412
  <DropdownItem onClick={() => { setMenuOpen(false); setConfirmClear(true) }}>
370
413
  Clear History
371
414
  </DropdownItem>
372
- {session.agentId && agents[session.agentId] && !isMainChat && (
373
- <DropdownItem danger onClick={() => { setMenuOpen(false); setConfirmDeleteAgent(true) }}>
374
- Delete Agent
375
- </DropdownItem>
376
- )}
377
415
  {!isMainChat && (
378
416
  <DropdownItem danger onClick={() => { setMenuOpen(false); setConfirmDelete(true) }}>
379
417
  Delete Chat
@@ -432,7 +470,21 @@ export function ChatArea() {
432
470
  <ChatPreviewPanel content={previewContent} onClose={() => setPreviewContent(null)} />
433
471
  )}
434
472
  {isDesktop && inspectorOpen && currentAgent && (
435
- <InspectorPanel agent={currentAgent} />
473
+ <InspectorPanel
474
+ agent={currentAgent}
475
+ onEditAgent={() => { setEditingAgentId(session.agentId!); setAgentSheetOpen(true) }}
476
+ onClearHistory={() => setConfirmClear(true)}
477
+ onDeleteAgent={!isMainChat ? () => setConfirmDeleteAgent(true) : undefined}
478
+ onDeleteChat={!isMainChat ? () => setConfirmDelete(true) : undefined}
479
+ isMainChat={isMainChat}
480
+ />
481
+ )}
482
+ {isDesktop && heartbeatHistoryOpen && currentAgent?.heartbeatEnabled && (
483
+ <HeartbeatHistoryPanel
484
+ messages={messages}
485
+ agentHeartbeatGoal={currentAgent.heartbeatGoal ?? undefined}
486
+ onClose={() => setHeartbeatHistoryOpen(false)}
487
+ />
436
488
  )}
437
489
  </div>
438
490
  )
@@ -442,7 +494,7 @@ function PromptIcon({ type }: { type: string }) {
442
494
  const cls = "w-5 h-5"
443
495
  switch (type) {
444
496
  case 'book':
445
- return <svg className={cls} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" style={{ color: '#818CF8' }}><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></svg>
497
+ return <svg className={cls} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" style={{ color: 'var(--color-accent-bright)' }}><rect x="3" y="3" width="7" height="7" rx="1" /><rect x="14" y="3" width="7" height="7" rx="1" /><rect x="3" y="14" width="7" height="7" rx="1" /><rect x="14" y="14" width="7" height="7" rx="1" /></svg>
446
498
  case 'link':
447
499
  return <svg className={cls} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" style={{ color: '#F472B6' }}><path d="M15 7h3a5 5 0 0 1 5 5 5 5 0 0 1-5 5h-3m-6 0H6a5 5 0 0 1-5-5 5 5 0 0 1 5-5h3" /><line x1="8" y1="12" x2="16" y2="12" /></svg>
448
500
  case 'bot':