@swarmclawai/swarmclaw 0.6.2 → 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.
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # SwarmClaw
2
2
 
3
3
  [![CI](https://github.com/swarmclawai/swarmclaw/actions/workflows/ci.yml/badge.svg)](https://github.com/swarmclawai/swarmclaw/actions/workflows/ci.yml)
4
- [![Release](https://img.shields.io/github/v/release/swarmclawai/swarmclaw?sort=semver)](https://github.com/swarmclawai/swarmclaw/releases)
4
+ [![Release](https://img.shields.io/github/v/tag/swarmclawai/swarmclaw)](https://github.com/swarmclawai/swarmclaw/releases)
5
+ [![npm](https://img.shields.io/npm/v/%40swarmclawai%2Fswarmclaw?label=npm)](https://www.npmjs.com/package/@swarmclawai/swarmclaw)
5
6
 
6
7
  <p align="center">
7
8
  <img src="https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/public/branding/swarmclaw-org-avatar.png" alt="SwarmClaw lobster logo" width="120" />
@@ -22,48 +23,6 @@ Inspired by [OpenClaw](https://github.com/openclaw).
22
23
  - Review agent system prompts before giving them shell or browser tools
23
24
  - Repeated failed access key attempts are rate-limited to slow brute-force attacks
24
25
 
25
- ## Features
26
-
27
- - **15 Built-in Providers** — Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, plus custom OpenAI-compatible endpoints
28
- - **OpenClaw Gateway** — Per-agent toggle to connect any agent to a local or remote OpenClaw gateway. Each agent gets its own gateway URL and token — run a swarm of OpenClaws from one dashboard. The `openclaw` CLI ships as a bundled dependency (no separate install needed)
29
- - **OpenClaw Control Plane** — Built-in gateway connection controls, reload mode switching (hot/hybrid/full), config issue detection/repair, remote history sync, and live execution approval handling
30
- - **Agent Builder** — Create agents with custom personalities (soul), system prompts, tools, and skills. AI-powered generation from a description
31
- - **Agent Inspector Panel** — Per-agent side panel for OpenClaw file editing (`SOUL.md`, `IDENTITY.md`, `USER.md`, etc.), guided personality editing, skill install/enable/remove, permission presets, sandbox env allowlist, and cron automations
32
- - **Agent Fleet Management** — Avatar seeds with generated avatars, running/approval fleet filters, soft-delete agent trash with restore/permanent delete, and approval counters in agent cards
33
- - **Agent Tools** — Shell, process control for long-running commands, files, edit file, send file, web search, web fetch, CLI delegation (Claude/Codex/OpenCode), Playwright browser automation, sub-agent spawning, canvas presentation, direct HTTP requests, git operations, persistent memory, and sandboxed code execution (JS/TS via Deno, Python)
34
- - **Platform Tools** — Agents can manage other agents, tasks, schedules, skills, connectors, sessions, and encrypted secrets via built-in platform tools
35
- - **Orchestration** — Multi-agent workflows powered by LangGraph with automatic sub-agent routing, checkpointed execution, and rich delegation cards that link to sub-agent chat threads
36
- - **Agentic Execution Policy** — Tool-first autonomous action loop with progress updates, evidence-driven answers, and better use of platform tools for long-lived work
37
- - **Runtime Date/Time Grounding** — Session, orchestrator, chatroom, and connector prompts include authoritative current timestamp context to reduce stale-date behavior
38
- - **Task Board** — Queue and track agent tasks with status, comments, results, and archiving. Strict capability policy pauses tasks for human approval before tool execution
39
- - **Task Metrics API** — Built-in analytics endpoint for WIP, cycle times, throughput velocity, completion/failure by agent, and priority distribution
40
- - **Background Daemon** — Auto-processes queued tasks and scheduled jobs with a 30s heartbeat plus recurring health monitoring
41
- - **Scheduling** — Cron-based agent scheduling with human-friendly presets
42
- - **Loop Runtime Controls** — Switch between bounded and ongoing loops with configurable step caps, runtime guards, heartbeat cadence, and timeout budgets
43
- - **Session Run Queue** — Per-session queued runs with followup/steer/collect modes, collect coalescing for bursty inputs, and run-state APIs
44
- - **Chat Iteration Workflow** — Edit-and-resend user turns, fork a new session from any message, bookmark key messages, use contextual follow-up suggestion chips, and auto-continue after tool access grants
45
- - **Agent Chatrooms** — Multi-agent room conversations with `@mention` routing, chained agent replies, reactions, and file/image-aware chat context
46
- - **Live Chat Telemetry** — Thinking/tool/responding stream phases, live main-loop status badges, connector activity presence, tone indicator, and optional sound notifications
47
- - **Global Search Palette** — `Cmd/Ctrl+K` search across agents, tasks, sessions, schedules, webhooks, and skills from anywhere in the app
48
- - **Notification Center** — Real-time in-app notifications for task/schedule/daemon events with unread tracking, mark-all/clear-read controls, and optional action links
49
- - **Preview-Rich Chat UI** — Side preview panel for tool outputs (image/browser/html/code), inline code/PDF previews for attachments, and image lightbox support
50
- - **Voice Settings** — Per-instance ElevenLabs API key + voice ID for TTS replies, plus configurable speech recognition language for chat input
51
- - **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling
52
- - **Skills System** — Discover local skills, import skills from URL, and load OpenClaw `SKILL.md` files (frontmatter-compatible)
53
- - **Execution Logging** — Structured audit trail for triggers, tool calls, file ops, commits, and errors in a dedicated `logs.db`
54
- - **Context Management** — Auto-compaction of conversation history when approaching context limits, with manual `context_status` and `context_summarize` tools for agents
55
- - **Memory** — Per-agent and per-session memory with hybrid FTS5 + vector embeddings search, relevance-based memory recall injected into runs, and periodic auto-journaling for durable execution context
56
- - **Cost Tracking** — Per-message token counting and cost estimation displayed in the chat header
57
- - **Provider Health Metrics** — Usage dashboard surfaces provider request volume, success rates, models used, and last-used timestamps
58
- - **Model Failover** — Automatic key rotation on rate limits and auth errors with configurable fallback credentials
59
- - **Plugin System** — Extend agent behavior with JS plugins (hooks: beforeAgentStart, afterAgentComplete, beforeToolExec, afterToolExec, onMessage)
60
- - **Secrets Vault** — Encrypted storage for API keys and service tokens
61
- - **Custom Providers** — Add any OpenAI-compatible API as a provider
62
- - **MCP Servers** — Connect agents to any Model Context Protocol server. Per-agent server selection with tool discovery and per-tool disable toggles
63
- - **Sandboxed Code Execution** — Agents can write and run JS/TS (Deno) or Python scripts in an isolated sandbox with network access, scoped filesystem, and artifact output
64
- - **Real-Time Sync** — WebSocket push notifications for instant UI updates across tabs and devices (fallback to polling when WS is unavailable)
65
- - **Mobile-First UI** — Responsive glass-themed dark interface, works on phone and desktop
66
-
67
26
  ## Requirements
68
27
 
69
28
  - **Node.js** 22.6+
@@ -88,7 +47,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
88
47
  ```
89
48
 
90
49
  The installer resolves the latest stable release tag and installs that version by default.
91
- To pin a version: `SWARMCLAW_VERSION=v0.6.1 curl ... | bash`
50
+ To pin a version: `SWARMCLAW_VERSION=v0.6.2 curl ... | bash`
92
51
 
93
52
  Or run locally from the repo (friendly for non-technical users):
94
53
 
@@ -146,6 +105,48 @@ Notes:
146
105
  - OpenClaw is configured per-agent via the **OpenClaw Gateway** toggle (not in the setup wizard).
147
106
  - You can skip setup and configure everything later in the sidebar.
148
107
 
108
+ ## Features
109
+
110
+ - **15 Built-in Providers** — Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, plus custom OpenAI-compatible endpoints
111
+ - **OpenClaw Gateway** — Per-agent toggle to connect any agent to a local or remote OpenClaw gateway. Each agent gets its own gateway URL and token — run a swarm of OpenClaws from one dashboard. The `openclaw` CLI ships as a bundled dependency (no separate install needed)
112
+ - **OpenClaw Control Plane** — Built-in gateway connection controls, reload mode switching (hot/hybrid/full), config issue detection/repair, remote history sync, and live execution approval handling
113
+ - **Agent Builder** — Create agents with custom personalities (soul), system prompts, tools, and skills. AI-powered generation from a description
114
+ - **Agent Inspector Panel** — Per-agent side panel for OpenClaw file editing (`SOUL.md`, `IDENTITY.md`, `USER.md`, etc.), guided personality editing, skill install/enable/remove, permission presets, sandbox env allowlist, and cron automations
115
+ - **Agent Fleet Management** — Avatar seeds with generated avatars, running/approval fleet filters, soft-delete agent trash with restore/permanent delete, and approval counters in agent cards
116
+ - **Agent Tools** — Shell, process control for long-running commands, files, edit file, send file, web search, web fetch, CLI delegation (Claude/Codex/OpenCode), Playwright browser automation, sub-agent spawning, canvas presentation, direct HTTP requests, git operations, persistent memory, and sandboxed code execution (JS/TS via Deno, Python)
117
+ - **Platform Tools** — Agents can manage other agents, tasks, schedules, skills, connectors, sessions, and encrypted secrets via built-in platform tools
118
+ - **Orchestration** — Multi-agent workflows powered by LangGraph with automatic sub-agent routing, checkpointed execution, and rich delegation cards that link to sub-agent chat threads
119
+ - **Agentic Execution Policy** — Tool-first autonomous action loop with progress updates, evidence-driven answers, and better use of platform tools for long-lived work
120
+ - **Runtime Date/Time Grounding** — Session, orchestrator, chatroom, and connector prompts include authoritative current timestamp context to reduce stale-date behavior
121
+ - **Task Board** — Queue and track agent tasks with status, comments, results, and archiving. Strict capability policy pauses tasks for human approval before tool execution
122
+ - **Task Metrics API** — Built-in analytics endpoint for WIP, cycle times, throughput velocity, completion/failure by agent, and priority distribution
123
+ - **Background Daemon** — Auto-processes queued tasks and scheduled jobs with a 30s heartbeat plus recurring health monitoring
124
+ - **Scheduling** — Cron-based agent scheduling with human-friendly presets
125
+ - **Loop Runtime Controls** — Switch between bounded and ongoing loops with configurable step caps, runtime guards, heartbeat cadence, and timeout budgets
126
+ - **Session Run Queue** — Per-session queued runs with followup/steer/collect modes, collect coalescing for bursty inputs, and run-state APIs
127
+ - **Chat Iteration Workflow** — Edit-and-resend user turns, fork a new session from any message, bookmark key messages, use contextual follow-up suggestion chips, and auto-continue after tool access grants
128
+ - **Agent Chatrooms** — Multi-agent room conversations with `@mention` routing, chained agent replies, reactions, and file/image-aware chat context
129
+ - **Live Chat Telemetry** — Thinking/tool/responding stream phases, live main-loop status badges, connector activity presence, tone indicator, and optional sound notifications
130
+ - **Global Search Palette** — `Cmd/Ctrl+K` search across agents, tasks, sessions, schedules, webhooks, and skills from anywhere in the app
131
+ - **Notification Center** — Real-time in-app notifications for task/schedule/daemon events with unread tracking, mark-all/clear-read controls, and optional action links
132
+ - **Preview-Rich Chat UI** — Side preview panel for tool outputs (image/browser/html/code), inline code/PDF previews for attachments, and image lightbox support
133
+ - **Voice Settings** — Per-instance ElevenLabs API key + voice ID for TTS replies, plus configurable speech recognition language for chat input
134
+ - **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling
135
+ - **Skills System** — Discover local skills, import skills from URL, and load OpenClaw `SKILL.md` files (frontmatter-compatible)
136
+ - **Execution Logging** — Structured audit trail for triggers, tool calls, file ops, commits, and errors in a dedicated `logs.db`
137
+ - **Context Management** — Auto-compaction of conversation history when approaching context limits, with manual `context_status` and `context_summarize` tools for agents
138
+ - **Memory** — Per-agent and per-session memory with hybrid FTS5 + vector embeddings search, relevance-based memory recall injected into runs, and periodic auto-journaling for durable execution context
139
+ - **Cost Tracking** — Per-message token counting and cost estimation displayed in the chat header
140
+ - **Provider Health Metrics** — Usage dashboard surfaces provider request volume, success rates, models used, and last-used timestamps
141
+ - **Model Failover** — Automatic key rotation on rate limits and auth errors with configurable fallback credentials
142
+ - **Plugin System** — Extend agent behavior with JS plugins (hooks: beforeAgentStart, afterAgentComplete, beforeToolExec, afterToolExec, onMessage)
143
+ - **Secrets Vault** — Encrypted storage for API keys and service tokens
144
+ - **Custom Providers** — Add any OpenAI-compatible API as a provider
145
+ - **MCP Servers** — Connect agents to any Model Context Protocol server. Per-agent server selection with tool discovery and per-tool disable toggles
146
+ - **Sandboxed Code Execution** — Agents can write and run JS/TS (Deno) or Python scripts in an isolated sandbox with network access, scoped filesystem, and artifact output
147
+ - **Real-Time Sync** — WebSocket push notifications for instant UI updates across tabs and devices (fallback to polling when WS is unavailable)
148
+ - **Mobile-First UI** — Responsive glass-themed dark interface, works on phone and desktop
149
+
149
150
  ## Configuration
150
151
 
151
152
  All config lives in `.env.local` (auto-generated):
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
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": {
@@ -1,41 +1,21 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSettings } from '@/lib/server/storage'
2
+ import { explainElevenLabsError, resolveElevenLabsConfig, synthesizeElevenLabsMp3 } from '@/lib/server/elevenlabs'
3
3
 
4
4
  export async function POST(req: Request) {
5
- const settings = loadSettings()
6
- const ELEVENLABS_KEY = settings.elevenLabsApiKey || process.env.ELEVENLABS_API_KEY
7
- const ELEVENLABS_VOICE = settings.elevenLabsVoiceId || process.env.ELEVENLABS_VOICE || 'JBFqnCBsd6RMkjVDRZzb'
8
-
9
- if (!ELEVENLABS_KEY) {
10
- return new NextResponse('No ElevenLabs API key. Set one in Settings > Voice.', { status: 500 })
5
+ try {
6
+ const { text, voiceId } = await req.json()
7
+ if (!String(text || '').trim()) {
8
+ return new NextResponse('No text provided', { status: 400 })
9
+ }
10
+ resolveElevenLabsConfig(voiceId)
11
+ const audioBuffer = await synthesizeElevenLabsMp3({ text: String(text || ''), voiceId })
12
+ return new NextResponse(new Uint8Array(audioBuffer), {
13
+ headers: {
14
+ 'Content-Type': 'audio/mpeg',
15
+ 'Cache-Control': 'no-cache',
16
+ },
17
+ })
18
+ } catch (err: unknown) {
19
+ return new NextResponse(explainElevenLabsError(err), { status: 500 })
11
20
  }
12
-
13
- const { text, voiceId } = await req.json()
14
- const voice = voiceId || ELEVENLABS_VOICE
15
- const apiRes = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voice}`, {
16
- method: 'POST',
17
- headers: {
18
- 'xi-api-key': ELEVENLABS_KEY,
19
- 'Content-Type': 'application/json',
20
- 'Accept': 'audio/mpeg',
21
- },
22
- body: JSON.stringify({
23
- text,
24
- model_id: 'eleven_multilingual_v2',
25
- voice_settings: { stability: 0.5, similarity_boost: 0.75 },
26
- }),
27
- })
28
-
29
- if (!apiRes.ok) {
30
- const err = await apiRes.text()
31
- return new NextResponse(err, { status: apiRes.status })
32
- }
33
-
34
- const audioBuffer = await apiRes.arrayBuffer()
35
- return new NextResponse(audioBuffer, {
36
- headers: {
37
- 'Content-Type': 'audio/mpeg',
38
- 'Cache-Control': 'no-cache',
39
- },
40
- })
41
21
  }
@@ -1,49 +1,20 @@
1
- import { loadSettings } from '@/lib/server/storage'
1
+ import { explainElevenLabsError, requestElevenLabsMp3Stream } from '@/lib/server/elevenlabs'
2
2
 
3
3
  export async function POST(req: Request) {
4
- const settings = loadSettings()
5
- const ELEVENLABS_KEY = settings.elevenLabsApiKey || process.env.ELEVENLABS_API_KEY
6
- const ELEVENLABS_VOICE = settings.elevenLabsVoiceId || process.env.ELEVENLABS_VOICE || 'JBFqnCBsd6RMkjVDRZzb'
7
-
8
- if (!ELEVENLABS_KEY) {
9
- return new Response('No ElevenLabs API key. Set one in Settings > Voice.', { status: 500 })
10
- }
11
-
12
- const { text, voiceId } = await req.json()
13
- if (!text?.trim()) {
14
- return new Response('No text provided', { status: 400 })
15
- }
16
-
17
- const voice = voiceId || ELEVENLABS_VOICE
18
- const apiRes = await fetch(
19
- `https://api.elevenlabs.io/v1/text-to-speech/${voice}/stream`,
20
- {
21
- method: 'POST',
4
+ try {
5
+ const { text, voiceId } = await req.json()
6
+ if (!String(text || '').trim()) {
7
+ return new Response('No text provided', { status: 400 })
8
+ }
9
+ const apiRes = await requestElevenLabsMp3Stream({ text: String(text || ''), voiceId })
10
+ return new Response(apiRes.body, {
22
11
  headers: {
23
- 'xi-api-key': ELEVENLABS_KEY,
24
- 'Content-Type': 'application/json',
25
- 'Accept': 'audio/mpeg',
12
+ 'Content-Type': 'audio/mpeg',
13
+ 'Transfer-Encoding': 'chunked',
14
+ 'Cache-Control': 'no-cache',
26
15
  },
27
- body: JSON.stringify({
28
- text: text.slice(0, 2000),
29
- model_id: 'eleven_multilingual_v2',
30
- voice_settings: { stability: 0.5, similarity_boost: 0.75 },
31
- output_format: 'mp3_22050_32',
32
- }),
33
- },
34
- )
35
-
36
- if (!apiRes.ok) {
37
- const err = await apiRes.text()
38
- return new Response(err, { status: apiRes.status })
16
+ })
17
+ } catch (err: unknown) {
18
+ return new Response(explainElevenLabsError(err), { status: 500 })
39
19
  }
40
-
41
- // Pipe the streaming response directly
42
- return new Response(apiRes.body, {
43
- headers: {
44
- 'Content-Type': 'audio/mpeg',
45
- 'Transfer-Encoding': 'chunked',
46
- 'Cache-Control': 'no-cache',
47
- },
48
- })
49
20
  }
@@ -1,6 +1,6 @@
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'
@@ -65,6 +65,26 @@ export function ChatArea() {
65
65
  const [browserActive, setBrowserActive] = useState(false)
66
66
  const [heartbeatHistoryOpen, setHeartbeatHistoryOpen] = useState(false)
67
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)
68
88
  const [isDragging, setIsDragging] = useState(false)
69
89
  const dragCounter = useRef(0)
70
90
  const setPendingImage = useChatStore((s) => s.setPendingImage)
@@ -277,6 +297,10 @@ export function ChatArea() {
277
297
  onVoiceToggle={handleVoiceToggle}
278
298
  heartbeatHistoryOpen={heartbeatHistoryOpen}
279
299
  onToggleHeartbeatHistory={() => setHeartbeatHistoryOpen((v) => !v)}
300
+ connectorSources={connectorSources}
301
+ connectorFilter={connectorFilter}
302
+ onConnectorFilterChange={setConnectorFilter}
303
+ hasMultipleSources={hasMultipleSources}
280
304
  />
281
305
  )}
282
306
  {!isDesktop && (
@@ -291,6 +315,10 @@ export function ChatArea() {
291
315
  voiceActive={voice.active}
292
316
  voiceSupported={voice.supported}
293
317
  onVoiceToggle={handleVoiceToggle}
318
+ connectorSources={connectorSources}
319
+ connectorFilter={connectorFilter}
320
+ onConnectorFilterChange={setConnectorFilter}
321
+ hasMultipleSources={hasMultipleSources}
294
322
  />
295
323
  )}
296
324
  <DevServerBar status={devServerStatus} onStop={handleStopDevServer} />
@@ -356,7 +384,7 @@ export function ChatArea() {
356
384
  </div>
357
385
  </div>
358
386
  ) : (
359
- <MessageList messages={messages} streaming={streamingForThisSession} />
387
+ <MessageList messages={messages} streaming={streamingForThisSession} connectorFilter={connectorFilter} />
360
388
  )}
361
389
 
362
390
  {voice.active && (
@@ -53,9 +53,13 @@ interface Props {
53
53
  voiceSupported?: boolean
54
54
  heartbeatHistoryOpen?: boolean
55
55
  onToggleHeartbeatHistory?: () => void
56
+ connectorSources?: Map<string, { platform: string; connectorName: string }>
57
+ connectorFilter?: string | null
58
+ onConnectorFilterChange?: (filter: string | null) => void
59
+ hasMultipleSources?: boolean
56
60
  }
57
61
 
58
- export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser, onVoiceToggle, voiceActive, voiceSupported, heartbeatHistoryOpen, onToggleHeartbeatHistory }: Props) {
62
+ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser, onVoiceToggle, voiceActive, voiceSupported, heartbeatHistoryOpen, onToggleHeartbeatHistory, connectorSources, connectorFilter, onConnectorFilterChange, hasMultipleSources }: Props) {
59
63
  const ttsEnabled = useChatStore((s) => s.ttsEnabled)
60
64
  const toggleTts = useChatStore((s) => s.toggleTts)
61
65
  const soundEnabled = useChatStore((s) => s.soundEnabled)
@@ -89,6 +93,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
89
93
  const [heartbeatSaving, setHeartbeatSaving] = useState(false)
90
94
  const [hbDropdownOpen, setHbDropdownOpen] = useState(false)
91
95
  const hbDropdownRef = useRef<HTMLDivElement>(null)
96
+ const [sourceDropdownOpen, setSourceDropdownOpen] = useState(false)
97
+ const sourceDropdownRef = useRef<HTMLDivElement>(null)
92
98
  const [mainLoopSaving, setMainLoopSaving] = useState(false)
93
99
  const [mainLoopError, setMainLoopError] = useState('')
94
100
  const [mainLoopNotice, setMainLoopNotice] = useState('')
@@ -399,6 +405,15 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
399
405
  return () => document.removeEventListener('mousedown', handler)
400
406
  }, [hbDropdownOpen])
401
407
 
408
+ useEffect(() => {
409
+ if (!sourceDropdownOpen) return
410
+ const handler = (e: MouseEvent) => {
411
+ if (sourceDropdownRef.current && !sourceDropdownRef.current.contains(e.target as Node)) setSourceDropdownOpen(false)
412
+ }
413
+ document.addEventListener('mousedown', handler)
414
+ return () => document.removeEventListener('mousedown', handler)
415
+ }, [sourceDropdownOpen])
416
+
402
417
  useEffect(() => {
403
418
  if (!modelSwitcherOpen) return
404
419
  const handler = (e: MouseEvent) => {
@@ -439,10 +454,11 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
439
454
  return () => clearTimeout(timer)
440
455
  }, [mainLoopNotice])
441
456
 
442
- // Context bar shows for tools, mission controls, memories, task links, resume handles, browser
457
+ // Context bar shows for tools, mission controls, memories, source filter, task links, resume handles, browser
443
458
  const hasToolToggles = ((agent?.tools?.length ?? 0) > 0) || ((session.tools?.length ?? 0) > 0)
444
459
  const hasMemoryLink = !!(agent && session.tools?.includes('memory'))
445
- const hasContextBar = !!(hasToolToggles || isMainSession || hasMemoryLink || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
460
+ const hasSourceFilter = !!hasMultipleSources
461
+ const hasContextBar = !!(hasToolToggles || isMainSession || hasMemoryLink || hasSourceFilter || linkedTask || resumeHandle || (isOpenClawAgent && openclawSessionKey) || browserActive)
446
462
 
447
463
  return (
448
464
  <header
@@ -863,6 +879,57 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
863
879
  Memories
864
880
  </button>
865
881
  )}
882
+ {hasSourceFilter && onConnectorFilterChange && connectorSources && (
883
+ <div className="relative shrink-0" ref={sourceDropdownRef}>
884
+ <button
885
+ onClick={() => setSourceDropdownOpen((o) => !o)}
886
+ className={`flex items-center gap-1 px-2 py-1 rounded-[7px] transition-colors cursor-pointer border-none text-[10px] font-600 shrink-0 ${
887
+ connectorFilter
888
+ ? 'bg-accent-soft/60 text-accent-bright/80 hover:bg-accent-soft'
889
+ : 'bg-white/[0.03] text-text-3/50 hover:bg-white/[0.06] hover:text-text-3/70'
890
+ }`}
891
+ title="Filter by message source"
892
+ >
893
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
894
+ <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
895
+ </svg>
896
+ {connectorFilter
897
+ ? (connectorSources.get(connectorFilter)?.connectorName || 'Source')
898
+ : 'Source'}
899
+ <svg width="7" height="7" viewBox="0 0 16 16" fill="none" className="opacity-40">
900
+ <path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
901
+ </svg>
902
+ </button>
903
+ {sourceDropdownOpen && (
904
+ <div className="absolute top-full left-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[140px]">
905
+ <button
906
+ onClick={() => { onConnectorFilterChange(null); setSourceDropdownOpen(false) }}
907
+ className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none flex items-center gap-2 ${
908
+ !connectorFilter ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'
909
+ }`}
910
+ >
911
+ All Sources
912
+ </button>
913
+ {Array.from(connectorSources.entries()).map(([cid, info]) => {
914
+ const active = connectorFilter === cid
915
+ const meta = CONNECTOR_PLATFORM_META[info.platform as keyof typeof CONNECTOR_PLATFORM_META]
916
+ return (
917
+ <button
918
+ key={cid}
919
+ onClick={() => { onConnectorFilterChange(active ? null : cid); setSourceDropdownOpen(false) }}
920
+ className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none flex items-center gap-2 ${
921
+ active ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'
922
+ }`}
923
+ >
924
+ <ConnectorPlatformIcon platform={info.platform as keyof typeof CONNECTOR_PLATFORM_META} size={12} />
925
+ {info.connectorName || meta?.label || info.platform}
926
+ </button>
927
+ )
928
+ })}
929
+ </div>
930
+ )}
931
+ </div>
932
+ )}
866
933
  {isOpenClawAgent && openclawSessionKey && (
867
934
  <>
868
935
  <button
@@ -6,7 +6,6 @@ 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
8
  import { AgentAvatar } from '@/components/agents/agent-avatar'
9
- import { ConnectorPlatformIcon, CONNECTOR_PLATFORM_META } from '@/components/shared/connector-platform-icon'
10
9
  import { MessageBubble } from './message-bubble'
11
10
  import { StreamingBubble } from './streaming-bubble'
12
11
  import { ThinkingIndicator } from './thinking-indicator'
@@ -47,9 +46,10 @@ function dateSeparator(ts: number): string {
47
46
  interface Props {
48
47
  messages: Message[]
49
48
  streaming: boolean
49
+ connectorFilter?: string | null
50
50
  }
51
51
 
52
- export function MessageList({ messages, streaming }: Props) {
52
+ export function MessageList({ messages, streaming, connectorFilter = null }: Props) {
53
53
  const scrollRef = useRef<HTMLDivElement>(null)
54
54
  const [showScrollToBottom, setShowScrollToBottom] = useState(false)
55
55
  const snapUntilRef = useRef(0)
@@ -119,9 +119,7 @@ export function MessageList({ messages, streaming }: Props) {
119
119
  // Bookmark filter
120
120
  const [bookmarkFilter, setBookmarkFilter] = useState(false)
121
121
 
122
- // Connector source filter
123
- const [connectorFilter, setConnectorFilter] = useState<string | null>(null)
124
- const [connectorFilterCollapsed, setConnectorFilterCollapsed] = useState(false)
122
+ // Connector filtering is handled via connectorFilter prop from chat-area
125
123
 
126
124
  const toggleBookmark = useCallback(async (index: number) => {
127
125
  if (!sessionId) return
@@ -181,17 +179,6 @@ export function MessageList({ messages, streaming }: Props) {
181
179
  }
182
180
  }
183
181
 
184
- // Collect unique connector sources for filter UI
185
- const connectorSources = new Map<string, { platform: string; connectorName: string }>()
186
- for (const msg of displayedMessages) {
187
- if (msg.source?.connectorId && !connectorSources.has(msg.source.connectorId)) {
188
- connectorSources.set(msg.source.connectorId, {
189
- platform: msg.source.platform,
190
- connectorName: msg.source.connectorName,
191
- })
192
- }
193
- }
194
-
195
182
  // Apply bookmark + connector filter
196
183
  let filteredMessages = bookmarkFilter
197
184
  ? displayedMessages.filter((msg) => msg.bookmarked)
@@ -407,61 +394,6 @@ export function MessageList({ messages, streaming }: Props) {
407
394
  </div>
408
395
  )}
409
396
 
410
- {/* Connector source filter — shown when connector messages exist */}
411
- {connectorSources.size > 0 && (
412
- <div className="flex items-center gap-1.5 px-6 md:px-12 lg:px-16 py-1.5 border-b border-white/[0.04]">
413
- <button
414
- onClick={() => setConnectorFilterCollapsed((c) => !c)}
415
- className="flex items-center gap-1 text-[10px] text-text-3/50 uppercase tracking-wider font-600 mr-1 bg-transparent border-none cursor-pointer hover:text-text-3/70 transition-colors p-0"
416
- title={connectorFilterCollapsed ? 'Expand source filter' : 'Collapse source filter'}
417
- >
418
- <svg
419
- width="8" height="8" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
420
- className="transition-transform duration-200"
421
- style={{ transform: connectorFilterCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)' }}
422
- >
423
- <polyline points="6 9 12 15 18 9" />
424
- </svg>
425
- Source
426
- {connectorFilterCollapsed && connectorFilter && (
427
- <span className="text-accent-bright/70 normal-case tracking-normal">
428
- ({connectorSources.get(connectorFilter)?.connectorName || connectorFilter})
429
- </span>
430
- )}
431
- </button>
432
- {!connectorFilterCollapsed && (
433
- <>
434
- <button
435
- onClick={() => setConnectorFilter(null)}
436
- className={`px-2 py-1 rounded-[6px] text-[11px] font-600 cursor-pointer border-none transition-all ${
437
- !connectorFilter ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]'
438
- }`}
439
- style={{ fontFamily: 'inherit' }}
440
- >
441
- All
442
- </button>
443
- {Array.from(connectorSources.entries()).map(([cid, info]) => {
444
- const active = connectorFilter === cid
445
- const meta = CONNECTOR_PLATFORM_META[info.platform as keyof typeof CONNECTOR_PLATFORM_META]
446
- return (
447
- <button
448
- key={cid}
449
- onClick={() => setConnectorFilter(active ? null : cid)}
450
- className={`flex items-center gap-1.5 px-2 py-1 rounded-[6px] text-[11px] font-600 cursor-pointer border-none transition-all ${
451
- active ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2 hover:bg-white/[0.04]'
452
- }`}
453
- style={{ fontFamily: 'inherit' }}
454
- >
455
- <ConnectorPlatformIcon platform={info.platform as keyof typeof CONNECTOR_PLATFORM_META} size={12} />
456
- {info.connectorName || meta?.label || info.platform}
457
- </button>
458
- )
459
- })}
460
- </>
461
- )}
462
- </div>
463
- )}
464
-
465
397
  <div
466
398
  ref={scrollRef}
467
399
  onScroll={updateScrollState}
@@ -222,6 +222,21 @@ const PLATFORMS: {
222
222
  },
223
223
  ]
224
224
 
225
+ const COMMON_CONFIG_FIELDS: { key: string; label: string; placeholder: string; help?: string }[] = [
226
+ {
227
+ key: 'taskFollowups',
228
+ label: 'Task Follow-ups',
229
+ placeholder: 'true | false',
230
+ help: 'Enable automatic connector follow-up messages when this agent completes or fails a task.',
231
+ },
232
+ {
233
+ key: 'taskFollowupTemplate',
234
+ label: 'Task Follow-up Template',
235
+ placeholder: 'Task {status}: {title}\\n\\n{summary}',
236
+ help: 'Optional placeholders: {status}, {title}, {summary}, {taskId}.',
237
+ },
238
+ ]
239
+
225
240
  export function ConnectorSheet() {
226
241
  const open = useAppStore((s) => s.connectorSheetOpen)
227
242
  const setOpen = useAppStore((s) => s.setConnectorSheetOpen)
@@ -625,7 +640,7 @@ export function ConnectorSheet() {
625
640
  )}
626
641
 
627
642
  {/* Platform-specific config */}
628
- {platformConfig.configFields.map((field) => {
643
+ {[...platformConfig.configFields, ...COMMON_CONFIG_FIELDS].map((field) => {
629
644
  const isTagField = field.key === 'allowedJids' || field.key === 'channelIds' || field.key === 'chatIds' || field.key === 'allowFrom'
630
645
  if (isTagField) {
631
646
  const tags = (config[field.key] || '').split(',').map((s) => s.trim()).filter(Boolean)