@swarmclawai/swarmclaw 0.7.6 → 0.7.8

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 (86) hide show
  1. package/README.md +19 -10
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +16 -0
  4. package/src/app/api/agents/route.ts +2 -0
  5. package/src/app/api/chats/[id]/route.ts +21 -1
  6. package/src/app/api/chats/route.ts +13 -1
  7. package/src/app/api/connectors/[id]/route.ts +20 -2
  8. package/src/app/api/connectors/route.ts +12 -8
  9. package/src/app/api/external-agents/[id]/heartbeat/route.ts +3 -0
  10. package/src/app/api/external-agents/[id]/route.ts +38 -6
  11. package/src/app/api/external-agents/route.ts +17 -1
  12. package/src/app/api/gateways/[id]/health/route.ts +8 -0
  13. package/src/app/api/gateways/[id]/route.ts +53 -1
  14. package/src/app/api/gateways/route.ts +53 -0
  15. package/src/app/api/openclaw/deploy/route.ts +139 -0
  16. package/src/app/api/projects/[id]/route.ts +6 -2
  17. package/src/app/api/projects/route.ts +4 -3
  18. package/src/app/api/secrets/[id]/route.ts +1 -0
  19. package/src/app/api/secrets/route.ts +2 -1
  20. package/src/app/api/settings/route.ts +2 -0
  21. package/src/cli/index.js +40 -0
  22. package/src/cli/index.test.js +68 -0
  23. package/src/cli/spec.js +60 -0
  24. package/src/components/agents/agent-sheet.tsx +281 -33
  25. package/src/components/auth/setup-wizard.tsx +75 -2
  26. package/src/components/chat/chat-area.tsx +36 -19
  27. package/src/components/chat/chat-header.tsx +4 -0
  28. package/src/components/chat/delegation-banner.test.ts +14 -1
  29. package/src/components/chat/delegation-banner.tsx +1 -1
  30. package/src/components/gateways/gateway-sheet.tsx +140 -8
  31. package/src/components/layout/app-layout.tsx +40 -23
  32. package/src/components/openclaw/openclaw-deploy-panel.tsx +591 -9
  33. package/src/components/projects/project-detail.tsx +217 -0
  34. package/src/components/projects/project-sheet.tsx +176 -4
  35. package/src/components/providers/provider-list.tsx +221 -17
  36. package/src/components/shared/settings/section-capability-policy.tsx +38 -0
  37. package/src/components/shared/settings/section-voice.tsx +11 -3
  38. package/src/components/tasks/approvals-panel.tsx +177 -18
  39. package/src/components/tasks/task-board.tsx +137 -23
  40. package/src/components/tasks/task-card.tsx +29 -0
  41. package/src/components/tasks/task-sheet.tsx +16 -4
  42. package/src/lib/server/agent-runtime-config.ts +142 -7
  43. package/src/lib/server/agent-thread-session.ts +9 -1
  44. package/src/lib/server/capability-router.test.ts +22 -0
  45. package/src/lib/server/capability-router.ts +54 -18
  46. package/src/lib/server/chat-execution.ts +33 -3
  47. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  48. package/src/lib/server/connectors/manager.ts +99 -74
  49. package/src/lib/server/daemon-state.ts +83 -46
  50. package/src/lib/server/elevenlabs.test.ts +59 -1
  51. package/src/lib/server/heartbeat-service.ts +5 -1
  52. package/src/lib/server/main-agent-loop.test.ts +260 -0
  53. package/src/lib/server/main-agent-loop.ts +559 -14
  54. package/src/lib/server/openclaw-deploy.test.ts +8 -0
  55. package/src/lib/server/openclaw-deploy.ts +679 -19
  56. package/src/lib/server/orchestrator-lg.ts +1 -0
  57. package/src/lib/server/orchestrator.ts +11 -0
  58. package/src/lib/server/plugins.ts +6 -1
  59. package/src/lib/server/project-context.ts +162 -0
  60. package/src/lib/server/project-utils.ts +150 -0
  61. package/src/lib/server/queue-followups.test.ts +147 -2
  62. package/src/lib/server/queue.ts +278 -8
  63. package/src/lib/server/session-run-manager.ts +31 -0
  64. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  65. package/src/lib/server/session-tools/connector.ts +26 -1
  66. package/src/lib/server/session-tools/context.ts +5 -0
  67. package/src/lib/server/session-tools/crud.ts +265 -76
  68. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  69. package/src/lib/server/session-tools/delegate.ts +38 -2
  70. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  71. package/src/lib/server/session-tools/memory.ts +14 -2
  72. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  73. package/src/lib/server/session-tools/platform.ts +60 -19
  74. package/src/lib/server/session-tools/web-inputs.test.ts +17 -0
  75. package/src/lib/server/session-tools/web.ts +153 -6
  76. package/src/lib/server/stream-agent-chat.test.ts +27 -2
  77. package/src/lib/server/stream-agent-chat.ts +104 -30
  78. package/src/lib/server/tool-aliases.ts +2 -0
  79. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  80. package/src/lib/server/tool-capability-policy.ts +29 -1
  81. package/src/lib/server/tool-planning.test.ts +44 -0
  82. package/src/lib/server/tool-planning.ts +269 -0
  83. package/src/lib/setup-defaults.ts +2 -2
  84. package/src/lib/tool-definitions.ts +2 -1
  85. package/src/lib/validation/schemas.ts +9 -0
  86. package/src/types/index.ts +104 -0
@@ -8,7 +8,7 @@ import { toast } from 'sonner'
8
8
  import { useWs } from '@/hooks/use-ws'
9
9
  import { ExecApprovalCard } from '@/components/chat/exec-approval-card'
10
10
  import { getApprovalPayload, getApprovalTitle } from '@/lib/approval-display'
11
- import type { ApprovalRequest } from '@/types'
11
+ import type { AppSettings, ApprovalCategory, ApprovalRequest } from '@/types'
12
12
 
13
13
  const CATEGORY_LABELS: Record<string, string> = {
14
14
  tool_access: 'Plugin Access',
@@ -28,6 +28,15 @@ const CATEGORY_ICONS: Record<string, string> = {
28
28
 
29
29
  type ApprovalScope = 'all' | 'execution' | 'workflow' | 'task'
30
30
 
31
+ const AUTO_APPROVE_OPTIONS: Array<{ id: ApprovalCategory; label: string; description: string }> = [
32
+ { id: 'tool_access', label: 'Plugin Access', description: 'Auto-enable requested plugins for a chat.' },
33
+ { id: 'plugin_scaffold', label: 'Plugin Scaffold', description: 'Auto-create plugin files requested by agents.' },
34
+ { id: 'plugin_install', label: 'Plugin Install', description: 'Auto-install plugins from approved URLs.' },
35
+ { id: 'human_loop', label: 'Human Approval Requests', description: 'Auto-approve ask-human approval prompts.' },
36
+ { id: 'wallet_transfer', label: 'Wallet Transfers', description: 'Auto-approve wallet send requests. High risk.' },
37
+ { id: 'task_tool', label: 'Task Tool Calls', description: 'Auto-approve task-level tool approvals.' },
38
+ ]
39
+
31
40
  function relativeTime(ts: number): string {
32
41
  const diff = Date.now() - ts
33
42
  if (diff < 60_000) return 'just now'
@@ -39,9 +48,11 @@ function relativeTime(ts: number): string {
39
48
  export function ApprovalsPanel() {
40
49
  const tasks = useAppStore((s) => s.tasks)
41
50
  const agents = useAppStore((s) => s.agents)
51
+ const appSettings = useAppStore((s) => s.appSettings)
42
52
  const serverApprovals = useAppStore((s) => s.approvals)
43
53
  const loadTasks = useAppStore((s) => s.loadTasks)
44
54
  const loadServerApprovals = useAppStore((s) => s.loadApprovals)
55
+ const loadAppSettings = useAppStore((s) => s.loadSettings)
45
56
 
46
57
  const execApprovals = useApprovalStore((s) => s.approvals)
47
58
  const loadExecApprovals = useApprovalStore((s) => s.loadApprovals)
@@ -73,12 +84,17 @@ export function ApprovalsPanel() {
73
84
  const [scope, setScope] = useState<ApprovalScope>('all')
74
85
  const [categoryFilter, setCategoryFilter] = useState('all')
75
86
  const [now, setNow] = useState(() => Date.now())
87
+ const [savingSetting, setSavingSetting] = useState<string | null>(null)
76
88
 
77
89
  useEffect(() => {
78
90
  const intervalId = window.setInterval(() => setNow(Date.now()), 60_000)
79
91
  return () => window.clearInterval(intervalId)
80
92
  }, [])
81
93
 
94
+ useEffect(() => {
95
+ void loadAppSettings()
96
+ }, [loadAppSettings])
97
+
82
98
  const taskApprovals = useMemo(() => {
83
99
  return Object.values(tasks)
84
100
  .filter((t) => t.pendingApproval)
@@ -180,6 +196,23 @@ export function ApprovalsPanel() {
180
196
  },
181
197
  ]
182
198
 
199
+ const autoApproved = useMemo(() => new Set(appSettings.approvalAutoApproveCategories || []), [appSettings.approvalAutoApproveCategories])
200
+ const approvalsEnabled = appSettings.approvalsEnabled ?? true
201
+ const outboundApprovalEnabled = appSettings.safetyRequireApprovalForOutbound ?? false
202
+
203
+ const saveApprovalSettings = async (patch: Partial<AppSettings>, successMessage: string, key: string) => {
204
+ try {
205
+ setSavingSetting(key)
206
+ const settings = await api<AppSettings>('PUT', '/settings', patch)
207
+ useAppStore.setState({ appSettings: settings })
208
+ toast.success(successMessage)
209
+ } catch (err: unknown) {
210
+ toast.error(err instanceof Error ? err.message : 'Failed to update approval settings')
211
+ } finally {
212
+ setSavingSetting((current) => (current === key ? null : current))
213
+ }
214
+ }
215
+
183
216
  const handleDecision = async (req: ApprovalRequest, approved: boolean) => {
184
217
  try {
185
218
  if (req.category === 'task_tool') {
@@ -195,23 +228,6 @@ export function ApprovalsPanel() {
195
228
  }
196
229
  }
197
230
 
198
- if (pendingCount === 0) {
199
- return (
200
- <div className="flex-1 flex flex-col items-center justify-center p-8 text-center">
201
- <div className="w-16 h-16 rounded-[24px] bg-white/[0.02] border border-white/[0.04] flex items-center justify-center mb-6">
202
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40">
203
- <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
204
- <path d="m9 12 2 2 4-4"/>
205
- </svg>
206
- </div>
207
- <h2 className="font-display text-[18px] font-600 text-text-2 mb-2">No pending approvals</h2>
208
- <p className="text-[13px] text-text-3/60 max-w-[320px]">
209
- Your swarm is operating autonomously. Actions requiring oversight will appear here.
210
- </p>
211
- </div>
212
- )
213
- }
214
-
215
231
  return (
216
232
  <div className="flex-1 overflow-y-auto px-6 py-8">
217
233
  <div className="max-w-3xl mx-auto">
@@ -237,6 +253,132 @@ export function ApprovalsPanel() {
237
253
  ))}
238
254
  </div>
239
255
 
256
+ <div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4 mb-6">
257
+ <div className="flex flex-col gap-4">
258
+ <div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-3">
259
+ <div>
260
+ <h2 className="text-[13px] font-700 text-text">Approval Controls</h2>
261
+ <p className="text-[12px] text-text-3/70 mt-1 max-w-[640px]">
262
+ Control whether actions queue for review, which approval types auto-run, and whether outbound connector sends need explicit confirmation.
263
+ </p>
264
+ </div>
265
+ <div className={`px-3 py-1.5 rounded-full text-[11px] font-700 ${
266
+ approvalsEnabled
267
+ ? 'bg-amber-500/10 border border-amber-500/20 text-amber-300'
268
+ : 'bg-emerald-500/10 border border-emerald-500/20 text-emerald-300'
269
+ }`}>
270
+ {approvalsEnabled ? 'Manual approvals enabled' : 'Approvals disabled'}
271
+ </div>
272
+ </div>
273
+
274
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
275
+ <div className="rounded-[12px] border border-white/[0.06] bg-black/20 px-4 py-4">
276
+ <div className="flex items-center justify-between gap-4">
277
+ <div>
278
+ <div className="text-[12px] font-600 text-text-2">Platform Approvals</div>
279
+ <p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">
280
+ Turn this off to auto-approve workflow approvals across the app. Audit records are still kept.
281
+ </p>
282
+ </div>
283
+ <button
284
+ type="button"
285
+ disabled={savingSetting === 'approvalsEnabled'}
286
+ onClick={() => {
287
+ const next = !approvalsEnabled
288
+ void saveApprovalSettings(
289
+ { approvalsEnabled: next },
290
+ next ? 'Platform approvals enabled' : 'Platform approvals disabled',
291
+ 'approvalsEnabled',
292
+ )
293
+ }}
294
+ className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer disabled:opacity-50 ${approvalsEnabled ? 'bg-accent' : 'bg-white/[0.12]'}`}
295
+ aria-label="Toggle platform approvals"
296
+ >
297
+ <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${approvalsEnabled ? 'translate-x-[18px]' : ''}`} />
298
+ </button>
299
+ </div>
300
+ </div>
301
+
302
+ <div className="rounded-[12px] border border-white/[0.06] bg-black/20 px-4 py-4">
303
+ <div className="flex items-center justify-between gap-4">
304
+ <div>
305
+ <div className="text-[12px] font-600 text-text-2">Outbound Send Approvals</div>
306
+ <p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">
307
+ Require explicit approval before agents send messages or media over connectors.
308
+ </p>
309
+ </div>
310
+ <button
311
+ type="button"
312
+ disabled={savingSetting === 'safetyRequireApprovalForOutbound'}
313
+ onClick={() => {
314
+ const next = !outboundApprovalEnabled
315
+ void saveApprovalSettings(
316
+ { safetyRequireApprovalForOutbound: next },
317
+ next ? 'Outbound send approvals enabled' : 'Outbound send approvals disabled',
318
+ 'safetyRequireApprovalForOutbound',
319
+ )
320
+ }}
321
+ className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer disabled:opacity-50 ${outboundApprovalEnabled ? 'bg-accent' : 'bg-white/[0.12]'}`}
322
+ aria-label="Toggle outbound send approvals"
323
+ >
324
+ <span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${outboundApprovalEnabled ? 'translate-x-[18px]' : ''}`} />
325
+ </button>
326
+ </div>
327
+ </div>
328
+ </div>
329
+
330
+ <div>
331
+ <div className="flex items-center justify-between gap-3 mb-2">
332
+ <div className="text-[12px] font-600 text-text-2">Auto-Approve Categories</div>
333
+ <div className="text-[11px] text-text-3/60">
334
+ {autoApproved.size} enabled
335
+ </div>
336
+ </div>
337
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
338
+ {AUTO_APPROVE_OPTIONS.map((option) => {
339
+ const checked = autoApproved.has(option.id)
340
+ return (
341
+ <label
342
+ key={option.id}
343
+ className={`rounded-[12px] border px-3 py-3 cursor-pointer transition-all ${
344
+ checked
345
+ ? 'border-accent-bright/30 bg-accent-soft/60'
346
+ : 'border-white/[0.06] bg-black/20 hover:bg-white/[0.04]'
347
+ }`}
348
+ >
349
+ <div className="flex items-start gap-3">
350
+ <input
351
+ type="checkbox"
352
+ checked={checked}
353
+ disabled={savingSetting === `auto:${option.id}`}
354
+ onChange={(e) => {
355
+ const next = new Set(appSettings.approvalAutoApproveCategories || [])
356
+ if (e.target.checked) next.add(option.id)
357
+ else next.delete(option.id)
358
+ void saveApprovalSettings(
359
+ { approvalAutoApproveCategories: [...next] },
360
+ checked ? `${option.label} now requires approval` : `${option.label} will auto-approve`,
361
+ `auto:${option.id}`,
362
+ )
363
+ }}
364
+ className="mt-0.5"
365
+ />
366
+ <div>
367
+ <div className="text-[12px] font-600 text-text-2">{option.label}</div>
368
+ <p className="text-[11px] text-text-3/60 mt-1 leading-relaxed">{option.description}</p>
369
+ </div>
370
+ </div>
371
+ </label>
372
+ )
373
+ })}
374
+ </div>
375
+ <p className="text-[11px] text-text-3/60 mt-2">
376
+ Use category auto-approval when you still want the approval system on, but you do not want these request types to pause execution.
377
+ </p>
378
+ </div>
379
+ </div>
380
+ </div>
381
+
240
382
  <div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4 mb-6">
241
383
  <div className="flex flex-col lg:flex-row gap-3 lg:items-center lg:justify-between">
242
384
  <div className="flex flex-wrap gap-2">
@@ -411,6 +553,23 @@ export function ApprovalsPanel() {
411
553
  <p className="text-[12px] text-text-3/60">Try clearing the search or switching the queue scope.</p>
412
554
  </div>
413
555
  )}
556
+
557
+ {pendingCount === 0 && (
558
+ <div className="flex flex-col items-center justify-center p-8 text-center">
559
+ <div className="w-16 h-16 rounded-[24px] bg-white/[0.02] border border-white/[0.04] flex items-center justify-center mb-6">
560
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40">
561
+ <path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z"/>
562
+ <path d="m9 12 2 2 4-4"/>
563
+ </svg>
564
+ </div>
565
+ <h2 className="font-display text-[18px] font-600 text-text-2 mb-2">No pending approvals</h2>
566
+ <p className="text-[13px] text-text-3/60 max-w-[360px]">
567
+ {approvalsEnabled
568
+ ? 'Your swarm is operating autonomously. Actions requiring oversight will appear here.'
569
+ : 'Approvals are currently disabled, so eligible requests will auto-run instead of queuing here.'}
570
+ </p>
571
+ </div>
572
+ )}
414
573
  </div>
415
574
  </div>
416
575
  )
@@ -14,11 +14,23 @@ import { toast } from 'sonner'
14
14
  const ACTIVE_COLUMNS: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed']
15
15
  type BoardViewMode = 'board' | 'list'
16
16
  type AttentionFilter = 'all' | 'needs-attention' | 'approval' | 'blocked' | 'overdue' | 'failed'
17
+ type TaskScopeFilter = 'user-facing' | 'all' | 'agent'
17
18
 
18
19
  function isTaskOverdue(task: BoardTask): boolean {
19
20
  return !!task.dueAt && task.dueAt < Date.now() && task.status !== 'completed' && task.status !== 'archived'
20
21
  }
21
22
 
23
+ function isInternalAgentTask(task: BoardTask): boolean {
24
+ if (task.sourceType === 'schedule' || task.sourceType === 'delegation') return true
25
+ return Boolean(task.createdByAgentId || task.delegatedByAgentId)
26
+ }
27
+
28
+ function isTaskRelevantToAgent(task: BoardTask, agentId: string): boolean {
29
+ return task.agentId === agentId
30
+ || task.createdByAgentId === agentId
31
+ || task.delegatedByAgentId === agentId
32
+ }
33
+
22
34
  function matchesAttentionFilter(task: BoardTask, filter: AttentionFilter): boolean {
23
35
  const blocked = !!task.blockedBy?.length
24
36
  const pendingApproval = !!task.pendingApproval
@@ -139,6 +151,13 @@ export function TaskBoard() {
139
151
  if (typeof window === 'undefined') return ''
140
152
  return new URLSearchParams(window.location.search).get('tag') || ''
141
153
  })
154
+ const [taskScopeFilter, setTaskScopeFilter] = useState<TaskScopeFilter>(() => {
155
+ if (typeof window === 'undefined') return 'user-facing'
156
+ const params = new URLSearchParams(window.location.search)
157
+ if (params.get('agent')) return 'agent'
158
+ const raw = params.get('taskView')
159
+ return raw === 'all' ? 'all' : 'user-facing'
160
+ })
142
161
  const [viewMode, setViewMode] = useState<BoardViewMode>('board')
143
162
  const [attentionFilter, setAttentionFilter] = useState<AttentionFilter>('all')
144
163
 
@@ -156,13 +175,14 @@ export function TaskBoard() {
156
175
  useEffect(() => {
157
176
  if (typeof window === 'undefined') return
158
177
  const params = new URLSearchParams()
159
- if (filterAgentId) params.set('agent', filterAgentId)
178
+ if (taskScopeFilter === 'agent' && filterAgentId) params.set('agent', filterAgentId)
179
+ else if (taskScopeFilter === 'all') params.set('taskView', 'all')
160
180
  if (filterTag) params.set('tag', filterTag)
161
181
  if (activeProjectFilter) params.set('project', activeProjectFilter)
162
182
  const qs = params.toString()
163
183
  const newUrl = `${window.location.pathname}${qs ? `?${qs}` : ''}`
164
184
  window.history.replaceState(null, '', newUrl)
165
- }, [filterAgentId, filterTag, activeProjectFilter])
185
+ }, [filterAgentId, filterTag, activeProjectFilter, taskScopeFilter])
166
186
 
167
187
  const [loaded, setLoaded] = useState(Object.keys(tasks).length > 0)
168
188
  useEffect(() => { Promise.all([loadTasks(), loadAgents(), loadProjects()]).then(() => setLoaded(true)) }, [])
@@ -173,17 +193,28 @@ export function TaskBoard() {
173
193
 
174
194
  const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
175
195
 
176
- const matchesBaseFilters = useCallback((task: BoardTask) => {
196
+ const matchesScopeFilters = useCallback((task: BoardTask) => {
177
197
  if (!showArchived && task.status === 'archived') return false
178
- if (filterAgentId && task.agentId !== filterAgentId) return false
198
+ if (taskScopeFilter === 'user-facing' && isInternalAgentTask(task)) return false
199
+ if (taskScopeFilter === 'agent' && (!filterAgentId || !isTaskRelevantToAgent(task, filterAgentId))) return false
179
200
  if (filterTag && !(task.tags && task.tags.includes(filterTag))) return false
180
201
  if (activeProjectFilter && task.projectId !== activeProjectFilter) return false
202
+ return true
203
+ }, [activeProjectFilter, filterAgentId, filterTag, showArchived, taskScopeFilter])
204
+
205
+ const matchesBaseFilters = useCallback((task: BoardTask) => {
206
+ if (!matchesScopeFilters(task)) return false
181
207
  if (!matchesAttentionFilter(task, attentionFilter)) return false
182
208
  return true
183
- }, [activeProjectFilter, attentionFilter, filterAgentId, filterTag, showArchived])
209
+ }, [attentionFilter, matchesScopeFilters])
210
+
211
+ const scopedTasks = useMemo(
212
+ () => Object.values(tasks).filter(matchesScopeFilters),
213
+ [tasks, matchesScopeFilters],
214
+ )
184
215
 
185
216
  const filteredTasks = useMemo(() => (
186
- Object.values(tasks)
217
+ scopedTasks
187
218
  .filter(matchesBaseFilters)
188
219
  .sort((a, b) => {
189
220
  const rankDiff = attentionRank(a) - attentionRank(b)
@@ -192,7 +223,7 @@ export function TaskBoard() {
192
223
  if (dueDiff !== 0) return dueDiff
193
224
  return b.updatedAt - a.updatedAt
194
225
  })
195
- ), [tasks, matchesBaseFilters])
226
+ ), [scopedTasks, matchesBaseFilters])
196
227
 
197
228
  const tasksByStatus = useCallback((status: BoardTaskStatus) =>
198
229
  filteredTasks
@@ -223,7 +254,7 @@ export function TaskBoard() {
223
254
 
224
255
  // Task counts per project (non-archived)
225
256
  const projectTaskCounts: Record<string, number> = {}
226
- for (const t of Object.values(tasks)) {
257
+ for (const t of scopedTasks) {
227
258
  if (t.projectId && t.status !== 'archived') {
228
259
  projectTaskCounts[t.projectId] = (projectTaskCounts[t.projectId] || 0) + 1
229
260
  }
@@ -231,7 +262,7 @@ export function TaskBoard() {
231
262
 
232
263
  // Summary stats
233
264
  const stats = useMemo(() => {
234
- const all = Object.values(tasks).filter((t) => t.status !== 'archived')
265
+ const all = scopedTasks.filter((t) => t.status !== 'archived')
235
266
  return {
236
267
  total: all.length,
237
268
  running: all.filter((t) => t.status === 'running').length,
@@ -242,7 +273,13 @@ export function TaskBoard() {
242
273
  approvals: all.filter((t) => !!t.pendingApproval).length,
243
274
  attention: all.filter((t) => matchesAttentionFilter(t, 'needs-attention')).length,
244
275
  }
245
- }, [tasks])
276
+ }, [scopedTasks])
277
+
278
+ const activeScopeLabel = useMemo(() => {
279
+ if (taskScopeFilter === 'all') return 'All tasks'
280
+ if (taskScopeFilter === 'agent' && filterAgentId && agents[filterAgentId]) return `${agents[filterAgentId].name} activity`
281
+ return 'User-facing tasks'
282
+ }, [agents, filterAgentId, taskScopeFilter])
246
283
 
247
284
  const activeAttentionLabel = useMemo(() => {
248
285
  if (attentionFilter === 'all') return null
@@ -301,6 +338,16 @@ export function TaskBoard() {
301
338
  <p className="text-[13px] text-text-3">
302
339
  {stats.total} task{stats.total !== 1 ? 's' : ''}
303
340
  </p>
341
+ <span className="inline-flex items-center gap-1 rounded-full bg-white/[0.04] px-2 py-1 text-[11px] font-600 text-text-2">
342
+ {taskScopeFilter === 'agent' && filterAgentId && agents[filterAgentId] ? (
343
+ <>
344
+ <AgentAvatar seed={agents[filterAgentId].avatarSeed || null} avatarUrl={agents[filterAgentId].avatarUrl} name={agents[filterAgentId].name} size={14} />
345
+ {activeScopeLabel}
346
+ </>
347
+ ) : (
348
+ activeScopeLabel
349
+ )}
350
+ </span>
304
351
  {stats.running > 0 && (
305
352
  <span className="inline-flex items-center gap-1 text-[11px] font-600 text-blue-400">
306
353
  <span className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse" />
@@ -338,41 +385,83 @@ export function TaskBoard() {
338
385
  <button
339
386
  onClick={() => setAgentDropdownOpen(!agentDropdownOpen)}
340
387
  className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
341
- ${filterAgentId
388
+ ${taskScopeFilter !== 'user-facing'
342
389
  ? 'bg-white/[0.06] border-white/[0.1] text-text-2'
343
390
  : 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
344
391
  style={{ fontFamily: 'inherit', minWidth: 130 }}
345
392
  >
346
- {filterAgentId && agents[filterAgentId] ? (
393
+ {taskScopeFilter === 'agent' && filterAgentId && agents[filterAgentId] ? (
347
394
  <>
348
395
  <AgentAvatar seed={agents[filterAgentId].avatarSeed || null} avatarUrl={agents[filterAgentId].avatarUrl} name={agents[filterAgentId].name} size={18} />
349
396
  {agents[filterAgentId].name}
350
397
  </>
351
- ) : 'All Agents'}
398
+ ) : taskScopeFilter === 'all' ? 'All Tasks' : 'User View'}
352
399
  <svg width="10" height="10" viewBox="0 0 10 10" fill="none" className="ml-auto opacity-50">
353
400
  <path d="M2.5 4L5 6.5L7.5 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
354
401
  </svg>
355
402
  </button>
356
403
  {agentDropdownOpen && (
357
- <div className="absolute top-full right-0 mt-1 min-w-[200px] py-1 rounded-[12px] border border-white/[0.08] bg-surface-2 shadow-lg z-50">
404
+ <div className="absolute top-full right-0 mt-1 min-w-[240px] py-1 rounded-[12px] border border-white/[0.08] bg-surface-2 shadow-lg z-50">
405
+ <button
406
+ onClick={() => {
407
+ setTaskScopeFilter('user-facing')
408
+ setFilterAgentId('')
409
+ setAgentDropdownOpen(false)
410
+ }}
411
+ className={`w-full flex items-start gap-2.5 px-3 py-2.5 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
412
+ ${taskScopeFilter === 'user-facing' ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
413
+ style={{ fontFamily: 'inherit' }}
414
+ >
415
+ <span className="mt-0.5 inline-flex h-5 items-center rounded-full bg-emerald-500/12 px-1.5 text-[10px] font-700 uppercase tracking-[0.08em] text-emerald-400">
416
+ Default
417
+ </span>
418
+ <span className="min-w-0">
419
+ <span className="block">User-facing tasks</span>
420
+ <span className="mt-0.5 block text-[11px] font-500 text-text-3/60">
421
+ Hide scheduled, delegated, and agent-created internal work.
422
+ </span>
423
+ </span>
424
+ </button>
358
425
  <button
359
- onClick={() => { setFilterAgentId(''); setAgentDropdownOpen(false) }}
360
- className={`w-full flex items-center gap-2.5 px-3 py-2 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
361
- ${!filterAgentId ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
426
+ onClick={() => {
427
+ setTaskScopeFilter('all')
428
+ setFilterAgentId('')
429
+ setAgentDropdownOpen(false)
430
+ }}
431
+ className={`w-full flex items-start gap-2.5 px-3 py-2.5 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
432
+ ${taskScopeFilter === 'all' ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
362
433
  style={{ fontFamily: 'inherit' }}
363
434
  >
364
- All Agents
435
+ <span className="mt-0.5 inline-flex h-5 items-center rounded-full bg-white/[0.06] px-1.5 text-[10px] font-700 uppercase tracking-[0.08em] text-text-3">
436
+ All
437
+ </span>
438
+ <span className="min-w-0">
439
+ <span className="block">All tasks</span>
440
+ <span className="mt-0.5 block text-[11px] font-500 text-text-3/60">
441
+ Include internal agent execution, schedules, and delegations.
442
+ </span>
443
+ </span>
365
444
  </button>
445
+ <div className="my-1 border-t border-white/[0.06]" />
366
446
  {Object.values(agents).sort((a, b) => a.name.localeCompare(b.name)).map((a) => (
367
447
  <button
368
448
  key={a.id}
369
- onClick={() => { setFilterAgentId(a.id); setAgentDropdownOpen(false) }}
449
+ onClick={() => {
450
+ setTaskScopeFilter('agent')
451
+ setFilterAgentId(a.id)
452
+ setAgentDropdownOpen(false)
453
+ }}
370
454
  className={`w-full flex items-center gap-2.5 px-3 py-2 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
371
- ${filterAgentId === a.id ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
455
+ ${taskScopeFilter === 'agent' && filterAgentId === a.id ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
372
456
  style={{ fontFamily: 'inherit' }}
373
457
  >
374
458
  <AgentAvatar seed={a.avatarSeed || null} avatarUrl={a.avatarUrl} name={a.name} size={20} />
375
- {a.name}
459
+ <span className="min-w-0 flex-1">
460
+ <span className="block truncate">{a.name}</span>
461
+ <span className="mt-0.5 block text-[11px] font-500 text-text-3/60">
462
+ Assigned, created, or delegated by this agent
463
+ </span>
464
+ </span>
376
465
  </button>
377
466
  ))}
378
467
  </div>
@@ -520,8 +609,33 @@ export function TaskBoard() {
520
609
  ))}
521
610
  </div>
522
611
 
523
- {(activeProjectFilter && projects[activeProjectFilter]) || activeAttentionLabel ? (
612
+ {(activeProjectFilter && projects[activeProjectFilter]) || activeAttentionLabel || taskScopeFilter !== 'all' ? (
524
613
  <div className="flex flex-wrap items-center gap-2 px-8 pb-3">
614
+ {taskScopeFilter !== 'all' && (
615
+ <span className={`inline-flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] border text-[12px] font-600 ${
616
+ taskScopeFilter === 'agent'
617
+ ? 'bg-accent-soft border-accent-bright/20 text-accent-bright'
618
+ : 'bg-emerald-500/10 border-emerald-500/20 text-emerald-400'
619
+ }`}>
620
+ {taskScopeFilter === 'agent' && filterAgentId && agents[filterAgentId] ? (
621
+ <>
622
+ <AgentAvatar seed={agents[filterAgentId].avatarSeed || null} avatarUrl={agents[filterAgentId].avatarUrl} name={agents[filterAgentId].name} size={14} />
623
+ {agents[filterAgentId].name} activity
624
+ </>
625
+ ) : (
626
+ 'User-facing tasks'
627
+ )}
628
+ <button
629
+ onClick={() => {
630
+ setTaskScopeFilter('all')
631
+ setFilterAgentId('')
632
+ }}
633
+ className="ml-1 cursor-pointer border-none bg-transparent p-0 text-[14px] leading-none text-current opacity-80 hover:opacity-100"
634
+ >
635
+ &times;
636
+ </button>
637
+ </span>
638
+ )}
525
639
  {activeProjectFilter && projects[activeProjectFilter] && (
526
640
  <span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] bg-white/[0.04] border border-white/[0.06] text-[12px] font-600 text-text-2">
527
641
  <span className="w-2 h-2 rounded-full" style={{ backgroundColor: projects[activeProjectFilter].color || '#6366F1' }} />
@@ -593,7 +707,7 @@ export function TaskBoard() {
593
707
  ) : filteredTasks.length === 0 ? (
594
708
  <div className="max-w-3xl mx-auto rounded-[16px] border border-dashed border-white/[0.08] px-6 py-14 text-center">
595
709
  <p className="text-[14px] font-600 text-text-2 mb-1">No tasks match this view</p>
596
- <p className="text-[12px] text-text-3/60">Try clearing one of the active filters or switching back to the full board.</p>
710
+ <p className="text-[12px] text-text-3/60">Try clearing one of the active filters or switching back to all tasks.</p>
597
711
  </div>
598
712
  ) : (
599
713
  <div className="max-w-4xl mx-auto">
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
6
  import { updateTask, archiveTask } from '@/lib/tasks'
7
7
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
8
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
8
9
  import type { BoardTask } from '@/types'
9
10
 
10
11
  function timeAgo(ts: number) {
@@ -46,6 +47,8 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect, index
46
47
  const tasks = useAppStore((s) => s.tasks)
47
48
  const agent = agents[task.agentId]
48
49
  const project = task.projectId ? projects[task.projectId] : null
50
+ const creatorAgent = task.createdByAgentId ? agents[task.createdByAgentId] : null
51
+ const delegatorAgent = task.delegatedByAgentId ? agents[task.delegatedByAgentId] : null
49
52
 
50
53
  const priorityConfig = {
51
54
  critical: { label: 'Critical', cls: 'bg-red-500/10 text-red-400' },
@@ -207,6 +210,32 @@ export function TaskCard({ task, selectionMode, selected, onToggleSelect, index
207
210
  </div>
208
211
  )}
209
212
 
213
+ {(creatorAgent || delegatorAgent || task.sourceType === 'schedule') && (
214
+ <div className="flex flex-wrap gap-1.5 mb-3">
215
+ {delegatorAgent && (
216
+ <span className="inline-flex items-center gap-1.5 rounded-[7px] bg-amber-500/10 px-2 py-1 text-[10px] font-600 text-amber-300">
217
+ <AgentAvatar seed={delegatorAgent.avatarSeed} avatarUrl={delegatorAgent.avatarUrl} name={delegatorAgent.name} size={14} />
218
+ Delegated by {delegatorAgent.name}
219
+ </span>
220
+ )}
221
+ {creatorAgent && creatorAgent.id !== delegatorAgent?.id && (
222
+ <span className="inline-flex items-center gap-1.5 rounded-[7px] bg-white/[0.05] px-2 py-1 text-[10px] font-600 text-text-2">
223
+ <AgentAvatar seed={creatorAgent.avatarSeed} avatarUrl={creatorAgent.avatarUrl} name={creatorAgent.name} size={14} />
224
+ Created by {creatorAgent.name}
225
+ </span>
226
+ )}
227
+ {task.sourceType === 'schedule' && (
228
+ <span className="inline-flex items-center gap-1.5 rounded-[7px] bg-purple-500/10 px-2 py-1 text-[10px] font-600 text-purple-300">
229
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
230
+ <circle cx="12" cy="12" r="8" />
231
+ <path d="M12 8v4l3 2" />
232
+ </svg>
233
+ {task.sourceScheduleName ? `Scheduled via ${task.sourceScheduleName}` : 'Scheduled task'}
234
+ </span>
235
+ )}
236
+ </div>
237
+ )}
238
+
210
239
  <div className="flex items-center gap-2 flex-wrap">
211
240
  {agent && (
212
241
  <span className="px-2 py-1 rounded-[6px] bg-accent-soft text-accent-bright text-[11px] font-600">
@@ -467,7 +467,7 @@ export function TaskSheet() {
467
467
  )}
468
468
 
469
469
  {/* CLI Sessions */}
470
- {(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.cliResumeId) && (
470
+ {(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId || editing.cliResumeId) && (
471
471
  <div className="mb-8">
472
472
  <SectionLabel>CLI Sessions</SectionLabel>
473
473
  <div className="flex flex-wrap gap-2">
@@ -489,7 +489,13 @@ export function TaskSheet() {
489
489
  <code className="text-[11px] text-text-3 font-mono">{editing.opencodeResumeId}</code>
490
490
  </div>
491
491
  )}
492
- {!(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId) && editing.cliResumeId && (
492
+ {editing.geminiResumeId && (
493
+ <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
494
+ <span className="text-[11px] font-600 text-fuchsia-400">Gemini</span>
495
+ <code className="text-[11px] text-text-3 font-mono">{editing.geminiResumeId}</code>
496
+ </div>
497
+ )}
498
+ {!(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId) && editing.cliResumeId && (
493
499
  <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
494
500
  <span className="text-[11px] font-600 text-text-2">{editing.cliProvider || 'CLI'}</span>
495
501
  <code className="text-[11px] text-text-3 font-mono">{editing.cliResumeId}</code>
@@ -971,7 +977,7 @@ export function TaskSheet() {
971
977
  </div>
972
978
  )}
973
979
 
974
- {editing && (editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.cliResumeId) && (
980
+ {editing && (editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId || editing.cliResumeId) && (
975
981
  <div className="mb-8">
976
982
  <SectionLabel>CLI Sessions</SectionLabel>
977
983
  <div className="flex flex-wrap gap-2">
@@ -993,7 +999,13 @@ export function TaskSheet() {
993
999
  <code className="text-[11px] text-text-3 font-mono">{editing.opencodeResumeId}</code>
994
1000
  </div>
995
1001
  )}
996
- {!(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId) && editing.cliResumeId && (
1002
+ {editing.geminiResumeId && (
1003
+ <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
1004
+ <span className="text-[11px] font-600 text-fuchsia-400">Gemini</span>
1005
+ <code className="text-[11px] text-text-3 font-mono">{editing.geminiResumeId}</code>
1006
+ </div>
1007
+ )}
1008
+ {!(editing.claudeResumeId || editing.codexResumeId || editing.opencodeResumeId || editing.geminiResumeId) && editing.cliResumeId && (
997
1009
  <div className="flex items-center gap-2 px-3 py-2 rounded-[10px] border border-white/[0.06] bg-surface">
998
1010
  <span className="text-[11px] font-600 text-text-2">{editing.cliProvider || 'CLI'}</span>
999
1011
  <code className="text-[11px] text-text-3 font-mono">{editing.cliResumeId}</code>