@swarmclawai/swarmclaw 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +15 -2
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +3 -2
  15. package/src/app/api/tts/stream/route.ts +3 -2
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +46 -22
  31. package/src/components/chat/chat-header.tsx +455 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +180 -7
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +68 -16
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +51 -11
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/manager.ts +218 -7
  78. package/src/lib/server/heartbeat-service.ts +8 -1
  79. package/src/lib/server/main-agent-loop.ts +1 -1
  80. package/src/lib/server/memory-consolidation.ts +15 -2
  81. package/src/lib/server/memory-db.ts +134 -6
  82. package/src/lib/server/mime.ts +51 -0
  83. package/src/lib/server/openclaw-gateway.ts +2 -2
  84. package/src/lib/server/orchestrator-lg.ts +2 -0
  85. package/src/lib/server/orchestrator.ts +5 -2
  86. package/src/lib/server/playwright-proxy.mjs +2 -3
  87. package/src/lib/server/prompt-runtime-context.ts +53 -0
  88. package/src/lib/server/queue.ts +52 -7
  89. package/src/lib/server/session-tools/canvas.ts +67 -0
  90. package/src/lib/server/session-tools/connector.ts +83 -9
  91. package/src/lib/server/session-tools/crud.ts +21 -0
  92. package/src/lib/server/session-tools/delegate.ts +68 -4
  93. package/src/lib/server/session-tools/git.ts +71 -0
  94. package/src/lib/server/session-tools/http.ts +57 -0
  95. package/src/lib/server/session-tools/index.ts +8 -0
  96. package/src/lib/server/session-tools/memory.ts +1 -0
  97. package/src/lib/server/session-tools/search-providers.ts +16 -8
  98. package/src/lib/server/session-tools/subagent.ts +106 -0
  99. package/src/lib/server/session-tools/web.ts +115 -4
  100. package/src/lib/server/stream-agent-chat.ts +32 -10
  101. package/src/lib/server/task-mention.ts +41 -0
  102. package/src/lib/sessions.ts +10 -0
  103. package/src/lib/soul-library.ts +103 -0
  104. package/src/lib/task-dedupe.ts +26 -0
  105. package/src/lib/tool-definitions.ts +2 -0
  106. package/src/lib/tts.ts +2 -2
  107. package/src/stores/use-app-store.ts +5 -1
  108. package/src/stores/use-chat-store.ts +65 -2
  109. package/src/types/index.ts +32 -2
@@ -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) {
@@ -232,9 +234,14 @@ export function ConnectorSheet() {
232
234
  const loadAgents = useAppStore((s) => s.loadAgents)
233
235
  const loadCredentials = useAppStore((s) => s.loadCredentials)
234
236
 
237
+ const chatrooms = useChatroomStore((s) => s.chatrooms)
238
+ const loadChatrooms = useChatroomStore((s) => s.loadChatrooms)
239
+
235
240
  const [name, setName] = useState('')
236
241
  const [platform, setPlatform] = useState<ConnectorPlatform>('discord')
237
242
  const [agentId, setAgentId] = useState('')
243
+ const [routeMode, setRouteMode] = useState<'agent' | 'chatroom'>('agent')
244
+ const [chatroomId, setChatroomId] = useState('')
238
245
  const [credentialId, setCredentialId] = useState('')
239
246
  const [config, setConfig] = useState<Record<string, string>>({})
240
247
  const [saving, setSaving] = useState(false)
@@ -255,8 +262,10 @@ export function ConnectorSheet() {
255
262
  if (open) {
256
263
  loadAgents()
257
264
  loadCredentials()
265
+ loadChatrooms()
258
266
  setShowSetup(false)
259
267
  }
268
+ // eslint-disable-next-line react-hooks/exhaustive-deps
260
269
  }, [open])
261
270
 
262
271
  // Sync form fields when editing connector changes (by ID, not reference)
@@ -265,13 +274,17 @@ export function ConnectorSheet() {
265
274
  if (editing) {
266
275
  setName(editing.name)
267
276
  setPlatform(editing.platform)
268
- setAgentId(editing.agentId)
277
+ setAgentId(editing.agentId || '')
278
+ setRouteMode(editing.chatroomId ? 'chatroom' : 'agent')
279
+ setChatroomId(editing.chatroomId || '')
269
280
  setCredentialId(editing.credentialId || '')
270
281
  setConfig(editing.config || {})
271
282
  } else {
272
283
  setName('')
273
284
  setPlatform('discord')
274
285
  setAgentId('')
286
+ setRouteMode('agent')
287
+ setChatroomId('')
275
288
  setCredentialId('')
276
289
  setConfig({})
277
290
  }
@@ -308,13 +321,17 @@ export function ConnectorSheet() {
308
321
  useWs('connectors', pollWaStatus, isWaRunning ? 2000 : undefined)
309
322
 
310
323
  const handleSave = async () => {
311
- if (!agentId) return
324
+ const hasTarget = routeMode === 'agent' ? !!agentId : !!chatroomId
325
+ if (!hasTarget) return
312
326
  setSaving(true)
327
+ const routePayload = routeMode === 'agent'
328
+ ? { agentId, chatroomId: null }
329
+ : { agentId: null, chatroomId }
313
330
  try {
314
331
  if (editing) {
315
- await api('PUT', `/connectors/${editing.id}`, { name, agentId, credentialId: credentialId || null, config })
332
+ await api('PUT', `/connectors/${editing.id}`, { name, ...routePayload, credentialId: credentialId || null, config })
316
333
  } else {
317
- await api('POST', '/connectors', { name: name || `${platformConfig?.label} Bot`, platform, agentId, credentialId: credentialId || null, config })
334
+ await api('POST', '/connectors', { name: name || `${platformConfig?.label} Bot`, platform, ...routePayload, credentialId: credentialId || null, config })
318
335
  }
319
336
  await loadConnectors()
320
337
  setOpen(false)
@@ -459,16 +476,51 @@ export function ConnectorSheet() {
459
476
  />
460
477
  </div>
461
478
 
462
- {/* Agent selector */}
479
+ {/* Route mode toggle + target selector */}
463
480
  <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
- />
481
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Route Messages To</label>
482
+ <div className="flex gap-1 mb-3 p-1 rounded-[10px] bg-white/[0.04] border border-white/[0.06]">
483
+ <button
484
+ type="button"
485
+ onClick={() => setRouteMode('agent')}
486
+ className={`flex-1 py-2 px-3 rounded-[8px] text-[13px] font-600 transition-all cursor-pointer border-none ${
487
+ routeMode === 'agent' ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
488
+ }`}
489
+ style={{ fontFamily: 'inherit' }}
490
+ >
491
+ Single Agent
492
+ </button>
493
+ <button
494
+ type="button"
495
+ onClick={() => setRouteMode('chatroom')}
496
+ className={`flex-1 py-2 px-3 rounded-[8px] text-[13px] font-600 transition-all cursor-pointer border-none ${
497
+ routeMode === 'chatroom' ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'
498
+ }`}
499
+ style={{ fontFamily: 'inherit' }}
500
+ >
501
+ Chat Room
502
+ </button>
503
+ </div>
504
+ {routeMode === 'agent' ? (
505
+ <>
506
+ <p className="text-[12px] text-text-3/60 mb-2">Incoming messages will be handled by this agent</p>
507
+ <AgentPickerList
508
+ agents={agentList}
509
+ selected={agentId}
510
+ onSelect={(id) => setAgentId(id)}
511
+ showOrchBadge={true}
512
+ />
513
+ </>
514
+ ) : (
515
+ <>
516
+ <p className="text-[12px] text-text-3/60 mb-2">Incoming messages will be routed to a chat room with multiple agents</p>
517
+ <ChatroomPickerList
518
+ chatrooms={Object.values(chatrooms)}
519
+ selected={chatroomId}
520
+ onSelect={(id) => setChatroomId(id)}
521
+ />
522
+ </>
523
+ )}
472
524
  </div>
473
525
 
474
526
  {/* Bot token credential */}
@@ -747,8 +799,8 @@ export function ConnectorSheet() {
747
799
  {editing && editing.platform === 'whatsapp' && (editing.status === 'running' || waConnecting) && !qrDataUrl && !waAuthenticated && (
748
800
  <div className="mb-6 p-5 rounded-[14px] border border-white/[0.06] bg-white/[0.01] text-center">
749
801
  <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]">
802
+ <span className="w-3 h-3 rounded-full border-2 border-blue-500 border-t-transparent animate-spin" />
803
+ <span className="text-[13px] font-600 text-blue-500">
752
804
  {waHasCreds ? 'Reconnecting...' : 'Waiting for QR code...'}
753
805
  </span>
754
806
  </div>
@@ -798,7 +850,7 @@ export function ConnectorSheet() {
798
850
  onCancel={() => { setOpen(false); setEditingId(null) }}
799
851
  onSave={handleSave}
800
852
  saveLabel={saving ? 'Saving...' : editing ? 'Save' : 'Create Connector'}
801
- saveDisabled={saving || !agentId}
853
+ saveDisabled={saving || (routeMode === 'agent' ? !agentId : !chatroomId)}
802
854
  left={editing && (
803
855
  <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
856
  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}