@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
@@ -2,27 +2,40 @@
2
2
 
3
3
  import { useCallback, useEffect, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { useChatroomStore } from '@/stores/use-chatroom-store'
5
6
  import { useWs } from '@/hooks/use-ws'
6
7
  import { api } from '@/lib/api-client'
7
8
  import type { Connector } from '@/types'
8
- import { ConnectorPlatformBadge, getConnectorPlatformLabel } from '@/components/shared/connector-platform-icon'
9
+ import { ConnectorPlatformIcon, ConnectorPlatformBadge, CONNECTOR_PLATFORM_META, getConnectorPlatformLabel } from '@/components/shared/connector-platform-icon'
10
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
9
11
 
10
- export function ConnectorList({ inSidebar: _inSidebar }: { inSidebar?: boolean }) {
11
- void _inSidebar
12
+ function relativeTime(ts: number): string {
13
+ const diff = Date.now() - ts
14
+ if (diff < 60_000) return 'just now'
15
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
16
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
17
+ const d = new Date(ts)
18
+ return d.toLocaleDateString([], { month: 'short', day: 'numeric' })
19
+ }
20
+
21
+ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
12
22
  const connectors = useAppStore((s) => s.connectors)
13
23
  const loadConnectors = useAppStore((s) => s.loadConnectors)
14
24
  const setConnectorSheetOpen = useAppStore((s) => s.setConnectorSheetOpen)
15
25
  const setEditingConnectorId = useAppStore((s) => s.setEditingConnectorId)
16
26
  const agents = useAppStore((s) => s.agents)
17
27
  const loadAgents = useAppStore((s) => s.loadAgents)
28
+ const chatrooms = useChatroomStore((s) => s.chatrooms)
29
+ const loadChatrooms = useChatroomStore((s) => s.loadChatrooms)
18
30
  const [toggling, setToggling] = useState<string | null>(null)
19
31
  const [reconnecting, setReconnecting] = useState<string | null>(null)
20
32
  const [loaded, setLoaded] = useState(false)
21
33
  const [error, setError] = useState<string | null>(null)
22
34
 
23
35
  const refresh = useCallback(async () => {
24
- await Promise.all([loadConnectors(), loadAgents()])
36
+ await Promise.all([loadConnectors(), loadAgents(), loadChatrooms()])
25
37
  setLoaded(true)
38
+ // eslint-disable-next-line react-hooks/exhaustive-deps
26
39
  }, [loadConnectors, loadAgents])
27
40
 
28
41
  useEffect(() => { void refresh() }, [refresh])
@@ -55,7 +68,6 @@ export function ConnectorList({ inSidebar: _inSidebar }: { inSidebar?: boolean }
55
68
  setReconnecting(c.id)
56
69
  setError(null)
57
70
  try {
58
- // Stop then start to reconnect
59
71
  try { await api('PUT', `/connectors/${c.id}`, { action: 'stop' }) } catch { /* may already be stopped */ }
60
72
  await api('PUT', `/connectors/${c.id}`, { action: 'start' })
61
73
  await refresh()
@@ -92,104 +104,170 @@ export function ConnectorList({ inSidebar: _inSidebar }: { inSidebar?: boolean }
92
104
  )
93
105
  }
94
106
 
107
+ // Sidebar: compact list layout
108
+ if (inSidebar) {
109
+ return (
110
+ <div className="flex-1 overflow-y-auto pb-20">
111
+ {error && (
112
+ <div className="mx-4 mt-2 mb-1 px-3 py-2 rounded-[8px] bg-red-500/10 border border-red-500/20 text-red-400 text-[11px] leading-snug">
113
+ {error}
114
+ </div>
115
+ )}
116
+ {list.map((c) => {
117
+ const agent = c.agentId ? agents[c.agentId] : null
118
+ const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
119
+ const isRunning = c.status === 'running'
120
+ const meta = CONNECTOR_PLATFORM_META[c.platform]
121
+ return (
122
+ <button
123
+ key={c.id}
124
+ onClick={() => { setEditingConnectorId(c.id); setConnectorSheetOpen(true) }}
125
+ className="w-full flex items-center gap-3 px-5 py-2.5 hover:bg-white/[0.02] transition-colors cursor-pointer bg-transparent border-none text-left"
126
+ >
127
+ <ConnectorPlatformIcon platform={c.platform} size={16} />
128
+ <div className="flex-1 min-w-0">
129
+ <span className="text-[13px] font-600 text-text truncate block">{c.name}</span>
130
+ <span className="text-[11px] text-text-3 truncate block">
131
+ {chatroom ? chatroom.name : agent?.name || meta?.label}
132
+ </span>
133
+ </div>
134
+ <span className={`shrink-0 w-2 h-2 rounded-full ${
135
+ isRunning ? 'bg-green-400' : c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
136
+ }`} />
137
+ </button>
138
+ )
139
+ })}
140
+ </div>
141
+ )
142
+ }
143
+
144
+ // Main view: card grid
95
145
  return (
96
- <div className="flex-1 overflow-y-auto pb-20">
146
+ <div className="flex-1 overflow-y-auto pb-20 px-5 pt-2">
97
147
  {error && (
98
- <div className="mx-4 mt-2 mb-1 px-3 py-2 rounded-[8px] bg-red-500/10 border border-red-500/20 text-red-400 text-[11px] leading-snug">
148
+ <div className="mb-3 px-3 py-2 rounded-[8px] bg-red-500/10 border border-red-500/20 text-red-400 text-[11px] leading-snug">
99
149
  {error}
100
150
  </div>
101
151
  )}
102
- {list.map((c) => {
103
- const platformLabel = getConnectorPlatformLabel(c.platform)
104
- const agent = agents[c.agentId]
105
- const isRunning = c.status === 'running'
106
- const isToggling = toggling === c.id
107
- // Can only toggle if connector has credentials (or uses non-token auth modes).
108
- const hasCredentials = c.platform === 'whatsapp'
109
- || c.platform === 'openclaw'
110
- || c.platform === 'signal'
111
- || (c.platform === 'bluebubbles' && (!!c.credentialId || !!c.config?.password))
112
- || !!c.credentialId
113
- return (
114
- <div
115
- key={c.id}
116
- className="w-full flex items-center gap-3 px-5 py-3 hover:bg-white/[0.02] transition-colors group"
117
- >
118
- {/* Clickable area — opens editor */}
152
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
153
+ {list.map((c) => {
154
+ const platformLabel = getConnectorPlatformLabel(c.platform)
155
+ const agent = c.agentId ? agents[c.agentId] : null
156
+ const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
157
+ const isRunning = c.status === 'running'
158
+ const isToggling = toggling === c.id
159
+ const hasCredentials = c.platform === 'whatsapp'
160
+ || c.platform === 'openclaw'
161
+ || c.platform === 'signal'
162
+ || (c.platform === 'bluebubbles' && (!!c.credentialId || !!c.config?.password))
163
+ || !!c.credentialId
164
+ const lastMsg = c.presence?.lastMessageAt
165
+
166
+ return (
119
167
  <button
168
+ key={c.id}
120
169
  onClick={() => { setEditingConnectorId(c.id); setConnectorSheetOpen(true) }}
121
- className="flex items-center gap-3 flex-1 min-w-0 cursor-pointer bg-transparent border-none text-left p-0"
170
+ className="group relative flex flex-col rounded-[14px] border border-white/[0.06] bg-surface p-4 cursor-pointer transition-all hover:border-white/[0.12] hover:bg-white/[0.02] text-left w-full"
171
+ style={{ fontFamily: 'inherit' }}
122
172
  >
123
- <ConnectorPlatformBadge platform={c.platform} size={36} iconSize={16} />
173
+ {/* Header: platform badge + status */}
174
+ <div className="flex items-center gap-3 mb-3">
175
+ <ConnectorPlatformBadge platform={c.platform} size={40} iconSize={20} roundedClassName="rounded-[10px]" />
176
+ <div className="flex-1 min-w-0">
177
+ <div className="flex items-center gap-2">
178
+ <span className="text-[14px] font-600 text-text truncate">{c.name}</span>
179
+ <span className={`shrink-0 w-2 h-2 rounded-full ${
180
+ isRunning ? 'bg-green-400' : c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
181
+ }`} />
182
+ </div>
183
+ <span className="text-[11px] text-text-3 block">
184
+ {isRunning ? 'Connected' : c.status === 'error' ? 'Error' : 'Stopped'}
185
+ {c.qrDataUrl && ' · QR ready'}
186
+ </span>
187
+ </div>
188
+ </div>
124
189
 
125
- <div className="flex-1 min-w-0">
126
- <div className="flex items-center gap-2">
127
- <span className="text-[13px] font-600 text-text truncate">{c.name}</span>
128
- <span
129
- className={`shrink-0 w-2 h-2 rounded-full ${
130
- isRunning ? 'bg-green-400' :
131
- c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
132
- }`}
133
- />
134
- {c.qrDataUrl && (
135
- <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-violet-400/80 bg-violet-400/[0.08] px-1.5 py-0.5 rounded-[5px]">
136
- QR Ready
137
- </span>
190
+ {/* Route target: agent or chatroom */}
191
+ <div className="flex items-center gap-2.5 mb-2.5 px-0.5">
192
+ {chatroom ? (
193
+ <>
194
+ <div className="w-6 h-6 rounded-full bg-white/[0.06] flex items-center justify-center shrink-0">
195
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3">
196
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
197
+ </svg>
198
+ </div>
199
+ <div className="flex-1 min-w-0">
200
+ <span className="text-[12px] font-600 text-text-2 block truncate">{chatroom.name}</span>
201
+ <span className="text-[10px] text-text-3/60 block">
202
+ {chatroom.agentIds.length} agent{chatroom.agentIds.length !== 1 ? 's' : ''}
203
+ {chatroom.chatMode === 'parallel' ? ' · parallel' : ' · sequential'}
204
+ </span>
205
+ </div>
206
+ </>
207
+ ) : agent ? (
208
+ <>
209
+ <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={24} />
210
+ <div className="flex-1 min-w-0">
211
+ <span className="text-[12px] font-600 text-text-2 block truncate">{agent.name}</span>
212
+ <span className="text-[10px] text-text-3/60 block">{agent.provider}/{agent.model}</span>
213
+ </div>
214
+ </>
215
+ ) : (
216
+ <span className="text-[11px] text-text-3/50">{platformLabel}</span>
217
+ )}
218
+ </div>
219
+
220
+ {/* Footer: last message time + error */}
221
+ <div className="flex items-center gap-2 mt-auto pt-2 border-t border-white/[0.04]">
222
+ {c.lastError ? (
223
+ <span className="text-[10px] text-red-400 truncate flex-1">
224
+ {c.lastError.slice(0, 50)}{c.lastError.length > 50 ? '...' : ''}
225
+ </span>
226
+ ) : lastMsg ? (
227
+ <span className="text-[10px] text-text-3/60 flex-1">Last message {relativeTime(lastMsg)}</span>
228
+ ) : (
229
+ <span className="text-[10px] text-text-3/40 flex-1">No messages yet</span>
230
+ )}
231
+
232
+ {/* Action buttons */}
233
+ <div className="flex gap-1 shrink-0" onClick={(e) => e.stopPropagation()}>
234
+ {c.status === 'error' && hasCredentials && (
235
+ <button
236
+ onClick={(e) => handleReconnect(e, c)}
237
+ disabled={reconnecting === c.id}
238
+ title="Reconnect"
239
+ className="px-2 py-1 rounded-[6px] text-[10px] font-600 transition-all cursor-pointer border-none opacity-0 group-hover:opacity-100 bg-amber-500/10 text-amber-400 hover:bg-amber-500/20 disabled:opacity-50"
240
+ >
241
+ {reconnecting === c.id ? '...' : 'Reconnect'}
242
+ </button>
243
+ )}
244
+ {hasCredentials && (
245
+ <button
246
+ onClick={(e) => handleToggle(e, c)}
247
+ disabled={isToggling}
248
+ title={isRunning ? 'Stop' : 'Start'}
249
+ className={`w-7 h-7 rounded-[6px] flex items-center justify-center transition-all cursor-pointer border-none ${
250
+ isToggling ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'
251
+ } ${isRunning
252
+ ? 'bg-red-500/10 text-red-400 hover:bg-red-500/20'
253
+ : 'bg-green-500/10 text-green-400 hover:bg-green-500/20'
254
+ } disabled:opacity-50`}
255
+ >
256
+ {isToggling ? (
257
+ <span className="w-3 h-3 rounded-full border-2 border-current border-t-transparent animate-spin" />
258
+ ) : isRunning ? (
259
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="16" height="16" rx="2" /></svg>
260
+ ) : (
261
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor"><polygon points="6,3 21,12 6,21" /></svg>
262
+ )}
263
+ </button>
138
264
  )}
139
- </div>
140
- <div className="text-[11px] text-text-3 truncate">
141
- {c.lastError
142
- ? <span className="text-red-400">{c.lastError.slice(0, 60)}{c.lastError.length > 60 ? '...' : ''}</span>
143
- : <>{platformLabel} {agent ? `\u2192 ${agent.name}` : ''}</>
144
- }
145
265
  </div>
146
266
  </div>
147
267
  </button>
148
-
149
- {/* Reconnect button for error-state connectors */}
150
- {c.status === 'error' && hasCredentials && (
151
- <button
152
- onClick={(e) => handleReconnect(e, c)}
153
- disabled={reconnecting === c.id}
154
- title="Reconnect"
155
- aria-label="Reconnect connector"
156
- className={`shrink-0 px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 transition-all cursor-pointer border-none
157
- ${reconnecting === c.id ? 'opacity-50' : 'opacity-0 group-hover:opacity-100 focus:opacity-100'}
158
- bg-amber-500/10 text-amber-400 hover:bg-amber-500/20`}
159
- >
160
- {reconnecting === c.id ? '...' : 'Reconnect'}
161
- </button>
162
- )}
163
-
164
- {/* Toggle button — visible on hover, only if connector has credentials */}
165
- {hasCredentials && <button
166
- onClick={(e) => handleToggle(e, c)}
167
- disabled={isToggling}
168
- title={isRunning ? 'Stop connector' : 'Start connector'}
169
- aria-label={isRunning ? 'Stop connector' : 'Start connector'}
170
- className={`shrink-0 w-8 h-8 rounded-[8px] flex items-center justify-center transition-all cursor-pointer border-none ${
171
- isToggling ? 'opacity-100' : 'opacity-0 group-hover:opacity-100 focus:opacity-100'
172
- } ${
173
- isRunning
174
- ? 'bg-red-500/10 text-red-400 hover:bg-red-500/20'
175
- : 'bg-green-500/10 text-green-400 hover:bg-green-500/20'
176
- } disabled:opacity-50`}
177
- >
178
- {isToggling ? (
179
- <span className="w-3 h-3 rounded-full border-2 border-current border-t-transparent animate-spin" />
180
- ) : isRunning ? (
181
- <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
182
- <rect x="4" y="4" width="16" height="16" rx="2" />
183
- </svg>
184
- ) : (
185
- <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
186
- <polygon points="6,3 21,12 6,21" />
187
- </svg>
188
- )}
189
- </button>}
190
- </div>
191
- )
192
- })}
268
+ )
269
+ })}
270
+ </div>
193
271
  </div>
194
272
  )
195
273
  }
@@ -9,8 +9,10 @@ import { toast } from 'sonner'
9
9
  import type { Connector, ConnectorPlatform } from '@/types'
10
10
  import { ConnectorPlatformBadge } from '@/components/shared/connector-platform-icon'
11
11
  import { AgentPickerList } from '@/components/shared/agent-picker-list'
12
+ import { ChatroomPickerList } from '@/components/shared/chatroom-picker-list'
12
13
  import { SheetFooter } from '@/components/shared/sheet-footer'
13
14
  import { SectionLabel } from '@/components/shared/section-label'
15
+ import { useChatroomStore } from '@/stores/use-chatroom-store'
14
16
 
15
17
  /** Auto-detect URLs in text and make them clickable links that open in a new tab */
16
18
  function linkify(text: string) {
@@ -220,6 +222,21 @@ const PLATFORMS: {
220
222
  },
221
223
  ]
222
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
+
223
240
  export function ConnectorSheet() {
224
241
  const open = useAppStore((s) => s.connectorSheetOpen)
225
242
  const setOpen = useAppStore((s) => s.setConnectorSheetOpen)
@@ -232,9 +249,14 @@ export function ConnectorSheet() {
232
249
  const loadAgents = useAppStore((s) => s.loadAgents)
233
250
  const loadCredentials = useAppStore((s) => s.loadCredentials)
234
251
 
252
+ const chatrooms = useChatroomStore((s) => s.chatrooms)
253
+ const loadChatrooms = useChatroomStore((s) => s.loadChatrooms)
254
+
235
255
  const [name, setName] = useState('')
236
256
  const [platform, setPlatform] = useState<ConnectorPlatform>('discord')
237
257
  const [agentId, setAgentId] = useState('')
258
+ const [routeMode, setRouteMode] = useState<'agent' | 'chatroom'>('agent')
259
+ const [chatroomId, setChatroomId] = useState('')
238
260
  const [credentialId, setCredentialId] = useState('')
239
261
  const [config, setConfig] = useState<Record<string, string>>({})
240
262
  const [saving, setSaving] = useState(false)
@@ -255,8 +277,10 @@ export function ConnectorSheet() {
255
277
  if (open) {
256
278
  loadAgents()
257
279
  loadCredentials()
280
+ loadChatrooms()
258
281
  setShowSetup(false)
259
282
  }
283
+ // eslint-disable-next-line react-hooks/exhaustive-deps
260
284
  }, [open])
261
285
 
262
286
  // Sync form fields when editing connector changes (by ID, not reference)
@@ -265,13 +289,17 @@ export function ConnectorSheet() {
265
289
  if (editing) {
266
290
  setName(editing.name)
267
291
  setPlatform(editing.platform)
268
- setAgentId(editing.agentId)
292
+ setAgentId(editing.agentId || '')
293
+ setRouteMode(editing.chatroomId ? 'chatroom' : 'agent')
294
+ setChatroomId(editing.chatroomId || '')
269
295
  setCredentialId(editing.credentialId || '')
270
296
  setConfig(editing.config || {})
271
297
  } else {
272
298
  setName('')
273
299
  setPlatform('discord')
274
300
  setAgentId('')
301
+ setRouteMode('agent')
302
+ setChatroomId('')
275
303
  setCredentialId('')
276
304
  setConfig({})
277
305
  }
@@ -308,13 +336,17 @@ export function ConnectorSheet() {
308
336
  useWs('connectors', pollWaStatus, isWaRunning ? 2000 : undefined)
309
337
 
310
338
  const handleSave = async () => {
311
- if (!agentId) return
339
+ const hasTarget = routeMode === 'agent' ? !!agentId : !!chatroomId
340
+ if (!hasTarget) return
312
341
  setSaving(true)
342
+ const routePayload = routeMode === 'agent'
343
+ ? { agentId, chatroomId: null }
344
+ : { agentId: null, chatroomId }
313
345
  try {
314
346
  if (editing) {
315
- await api('PUT', `/connectors/${editing.id}`, { name, agentId, credentialId: credentialId || null, config })
347
+ await api('PUT', `/connectors/${editing.id}`, { name, ...routePayload, credentialId: credentialId || null, config })
316
348
  } else {
317
- await api('POST', '/connectors', { name: name || `${platformConfig?.label} Bot`, platform, agentId, credentialId: credentialId || null, config })
349
+ await api('POST', '/connectors', { name: name || `${platformConfig?.label} Bot`, platform, ...routePayload, credentialId: credentialId || null, config })
318
350
  }
319
351
  await loadConnectors()
320
352
  setOpen(false)
@@ -459,16 +491,51 @@ export function ConnectorSheet() {
459
491
  />
460
492
  </div>
461
493
 
462
- {/* Agent selector */}
494
+ {/* Route mode toggle + target selector */}
463
495
  <div className="mb-6">
464
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Route to Agent</label>
465
- <p className="text-[12px] text-text-3/60 mb-2">Incoming messages will be handled by this agent</p>
466
- <AgentPickerList
467
- agents={agentList}
468
- selected={agentId}
469
- onSelect={(id) => setAgentId(id)}
470
- showOrchBadge={true}
471
- />
496
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Route Messages To</label>
497
+ <div className="flex gap-1 mb-3 p-1 rounded-[10px] bg-white/[0.04] border border-white/[0.06]">
498
+ <button
499
+ type="button"
500
+ onClick={() => setRouteMode('agent')}
501
+ className={`flex-1 py-2 px-3 rounded-[8px] text-[13px] font-600 transition-all cursor-pointer border-none ${
502
+ routeMode === 'agent' ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
503
+ }`}
504
+ style={{ fontFamily: 'inherit' }}
505
+ >
506
+ Single Agent
507
+ </button>
508
+ <button
509
+ type="button"
510
+ onClick={() => setRouteMode('chatroom')}
511
+ className={`flex-1 py-2 px-3 rounded-[8px] text-[13px] font-600 transition-all cursor-pointer border-none ${
512
+ routeMode === 'chatroom' ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
513
+ }`}
514
+ style={{ fontFamily: 'inherit' }}
515
+ >
516
+ Chat Room
517
+ </button>
518
+ </div>
519
+ {routeMode === 'agent' ? (
520
+ <>
521
+ <p className="text-[12px] text-text-3/60 mb-2">Incoming messages will be handled by this agent</p>
522
+ <AgentPickerList
523
+ agents={agentList}
524
+ selected={agentId}
525
+ onSelect={(id) => setAgentId(id)}
526
+ showOrchBadge={true}
527
+ />
528
+ </>
529
+ ) : (
530
+ <>
531
+ <p className="text-[12px] text-text-3/60 mb-2">Incoming messages will be routed to a chat room with multiple agents</p>
532
+ <ChatroomPickerList
533
+ chatrooms={Object.values(chatrooms)}
534
+ selected={chatroomId}
535
+ onSelect={(id) => setChatroomId(id)}
536
+ />
537
+ </>
538
+ )}
472
539
  </div>
473
540
 
474
541
  {/* Bot token credential */}
@@ -573,7 +640,7 @@ export function ConnectorSheet() {
573
640
  )}
574
641
 
575
642
  {/* Platform-specific config */}
576
- {platformConfig.configFields.map((field) => {
643
+ {[...platformConfig.configFields, ...COMMON_CONFIG_FIELDS].map((field) => {
577
644
  const isTagField = field.key === 'allowedJids' || field.key === 'channelIds' || field.key === 'chatIds' || field.key === 'allowFrom'
578
645
  if (isTagField) {
579
646
  const tags = (config[field.key] || '').split(',').map((s) => s.trim()).filter(Boolean)
@@ -747,8 +814,8 @@ export function ConnectorSheet() {
747
814
  {editing && editing.platform === 'whatsapp' && (editing.status === 'running' || waConnecting) && !qrDataUrl && !waAuthenticated && (
748
815
  <div className="mb-6 p-5 rounded-[14px] border border-white/[0.06] bg-white/[0.01] text-center">
749
816
  <div className="flex items-center justify-center gap-2 mb-1">
750
- <span className="w-3 h-3 rounded-full border-2 border-[#3B82F6] border-t-transparent animate-spin" />
751
- <span className="text-[13px] font-600 text-[#3B82F6]">
817
+ <span className="w-3 h-3 rounded-full border-2 border-blue-500 border-t-transparent animate-spin" />
818
+ <span className="text-[13px] font-600 text-blue-500">
752
819
  {waHasCreds ? 'Reconnecting...' : 'Waiting for QR code...'}
753
820
  </span>
754
821
  </div>
@@ -798,7 +865,7 @@ export function ConnectorSheet() {
798
865
  onCancel={() => { setOpen(false); setEditingId(null) }}
799
866
  onSave={handleSave}
800
867
  saveLabel={saving ? 'Saving...' : editing ? 'Save' : 'Create Connector'}
801
- saveDisabled={saving || !agentId}
868
+ saveDisabled={saving || (routeMode === 'agent' ? !agentId : !chatroomId)}
802
869
  left={editing && (
803
870
  <button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
804
871
  Delete
@@ -354,7 +354,7 @@ export function HomeView() {
354
354
  >
355
355
  <div className="relative">
356
356
  <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={36} />
357
- <div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-[#1a1a2e] ${
357
+ <div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-surface ${
358
358
  isTyping ? 'bg-accent-bright animate-pulse'
359
359
  : isOnline ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]'
360
360
  : 'bg-text-3/30'
@@ -7,6 +7,7 @@ import { uploadImage } from '@/lib/upload'
7
7
  import { useAutoResize } from '@/hooks/use-auto-resize'
8
8
  import { useSpeechRecognition } from '@/hooks/use-speech-recognition'
9
9
  import { FilePreview } from '@/components/shared/file-preview'
10
+ import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
10
11
 
11
12
  interface Props {
12
13
  streaming: boolean
@@ -91,8 +92,8 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
91
92
  try {
92
93
  const result = await uploadImage(file)
93
94
  addPendingFile({ file, path: result.path, url: result.url })
94
- } catch {
95
- // ignore upload errors
95
+ } catch (err: unknown) {
96
+ console.error('File upload failed:', err instanceof Error ? err.message : String(err))
96
97
  }
97
98
  }, [addPendingFile])
98
99
 
@@ -226,6 +227,31 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
226
227
  </button>
227
228
  )}
228
229
 
230
+ <Tooltip>
231
+ <TooltipTrigger asChild>
232
+ <button
233
+ type="button"
234
+ onClick={() => { useChatStore.getState().clearContext() }}
235
+ disabled={streaming}
236
+ className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
237
+ text-text-3 text-[13px] cursor-pointer hover:text-amber-400 hover:bg-amber-400/10 transition-all duration-200 disabled:opacity-30 disabled:pointer-events-none"
238
+ style={{ fontFamily: 'inherit' }}
239
+ >
240
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
241
+ <line x1="2" y1="12" x2="22" y2="12" />
242
+ <polyline points="8 8 4 12 8 16" />
243
+ <polyline points="16 8 20 12 16 16" />
244
+ </svg>
245
+ <span className="hidden sm:inline">New context</span>
246
+ </button>
247
+ </TooltipTrigger>
248
+ <TooltipContent side="top" sideOffset={8}
249
+ className="bg-raised border border-white/[0.08] text-text shadow-[0_8px_32px_rgba(0,0,0,0.5)] rounded-[10px] px-3.5 py-2.5 max-w-[220px]">
250
+ <div className="font-display text-[12px] font-600 mb-0.5">New context window</div>
251
+ <div className="text-[11px] text-text-3 leading-[1.4]">Adds a marker — messages above it won&apos;t be sent to the AI. Nothing is deleted.</div>
252
+ </TooltipContent>
253
+ </Tooltip>
254
+
229
255
  <div className="flex-1" />
230
256
 
231
257
  <span className="text-[11px] text-text-3/60 tabular-nums mr-2 font-mono">
@@ -55,6 +55,7 @@ import { MobileHeader } from './mobile-header'
55
55
  import { DaemonIndicator } from './daemon-indicator'
56
56
  import { NotificationCenter } from '@/components/shared/notification-center'
57
57
  import { ChatArea } from '@/components/chat/chat-area'
58
+ import { CanvasPanel } from '@/components/canvas/canvas-panel'
58
59
  import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
59
60
  import { api } from '@/lib/api-client'
60
61
  import type { AppView } from '@/types'
@@ -91,6 +92,7 @@ export function AppLayout() {
91
92
  const appSettings = useAppStore((s) => s.appSettings)
92
93
  const [agentViewMode, setAgentViewMode] = useState<'chat' | 'config'>('chat')
93
94
  const [profileSheetOpen, setProfileSheetOpen] = useState(false)
95
+ const [canvasDismissedFor, setCanvasDismissedFor] = useState<string | null>(null)
94
96
 
95
97
  const handleShortcutKey = useCallback((e: KeyboardEvent) => {
96
98
  const mod = e.metaKey || e.ctrlKey
@@ -192,6 +194,10 @@ export function AppLayout() {
192
194
  : Object.values(agents)[0]?.id || null
193
195
  const isMainChat = activeView === 'agents' && currentAgentId === defaultAgentId
194
196
 
197
+ const currentSession = currentSessionId ? sessions[currentSessionId] : null
198
+ const hasCanvas = !!(currentSession?.canvasContent && canvasDismissedFor !== currentSessionId)
199
+ const canvasAgentName = currentSession?.agentId && agents[currentSession.agentId] ? agents[currentSession.agentId].name : undefined
200
+
195
201
  const goToMainChat = async () => {
196
202
  if (defaultAgentId) {
197
203
  await setCurrentAgent(defaultAgentId)
@@ -680,12 +686,23 @@ export function AppLayout() {
680
686
 
681
687
  {/* Main content */}
682
688
  <ErrorBoundary>
683
- <div className="flex-1 flex flex-col h-full min-w-0 bg-bg">
689
+ <div className="flex-1 flex flex-col h-full min-h-0 min-w-0 bg-bg">
684
690
  {!isDesktop && <MobileHeader />}
685
691
  {activeView === 'home' ? (
686
692
  <HomeView />
687
693
  ) : activeView === 'agents' && hasSelectedSession ? (
688
- <ChatArea />
694
+ <div className="flex-1 flex h-full min-h-0 min-w-0">
695
+ <div className="flex-1 min-h-0 min-w-0 overflow-hidden">
696
+ <ChatArea />
697
+ </div>
698
+ {hasCanvas && currentSessionId && (
699
+ <CanvasPanel
700
+ sessionId={currentSessionId}
701
+ agentName={canvasAgentName}
702
+ onClose={() => setCanvasDismissedFor(currentSessionId)}
703
+ />
704
+ )}
705
+ </div>
689
706
  ) : activeView === 'agents' ? (
690
707
  <div className="flex-1 flex flex-col">
691
708
  {!isDesktop ? (
@@ -44,7 +44,7 @@ function AssignAgentPicker({ projectId, onClose }: { projectId: string; onClose:
44
44
  return (
45
45
  <>
46
46
  <div className="fixed inset-0 z-40" onClick={onClose} />
47
- <div className="absolute left-0 top-full mt-2 z-50 w-[260px] rounded-[12px] bg-[#1a1a2e]/95 backdrop-blur-xl border border-white/[0.1] shadow-[0_12px_40px_rgba(0,0,0,0.5)] overflow-hidden">
47
+ <div className="absolute left-0 top-full mt-2 z-50 w-[260px] rounded-[12px] bg-surface/95 backdrop-blur-xl border border-white/[0.1] shadow-[0_12px_40px_rgba(0,0,0,0.5)] overflow-hidden">
48
48
  <div className="p-2.5 border-b border-white/[0.06]">
49
49
  <input
50
50
  value={query}