@swarmclawai/swarmclaw 0.7.1 → 0.7.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 (237) hide show
  1. package/README.md +155 -150
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +37 -9
  5. package/src/app/api/agents/route.ts +13 -2
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
  9. package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
  10. package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
  11. package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
  12. package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
  13. package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
  14. package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
  15. package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
  16. package/src/app/api/{sessions → chats}/route.ts +21 -7
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  18. package/src/app/api/connectors/doctor/route.ts +13 -0
  19. package/src/app/api/files/open/route.ts +16 -14
  20. package/src/app/api/memory/maintenance/route.ts +11 -1
  21. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  22. package/src/app/api/openclaw/skills/route.ts +11 -3
  23. package/src/app/api/plugins/dependencies/route.ts +24 -0
  24. package/src/app/api/plugins/install/route.ts +15 -92
  25. package/src/app/api/plugins/route.ts +6 -26
  26. package/src/app/api/plugins/settings/route.ts +40 -0
  27. package/src/app/api/plugins/ui/route.ts +1 -0
  28. package/src/app/api/settings/route.ts +49 -7
  29. package/src/app/api/tasks/[id]/route.ts +15 -6
  30. package/src/app/api/tasks/bulk/route.ts +2 -2
  31. package/src/app/api/tasks/route.ts +9 -4
  32. package/src/app/api/usage/route.ts +30 -0
  33. package/src/app/api/webhooks/[id]/route.ts +8 -1
  34. package/src/app/page.tsx +9 -2
  35. package/src/cli/index.js +39 -33
  36. package/src/cli/index.ts +43 -49
  37. package/src/cli/spec.js +29 -27
  38. package/src/components/agents/agent-card.tsx +16 -13
  39. package/src/components/agents/agent-chat-list.tsx +104 -4
  40. package/src/components/agents/agent-list.tsx +54 -22
  41. package/src/components/agents/agent-sheet.tsx +209 -18
  42. package/src/components/agents/cron-job-form.tsx +3 -3
  43. package/src/components/agents/inspector-panel.tsx +110 -50
  44. package/src/components/auth/access-key-gate.tsx +36 -97
  45. package/src/components/auth/setup-wizard.tsx +5 -38
  46. package/src/components/chat/chat-area.tsx +39 -27
  47. package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
  48. package/src/components/chat/chat-header.tsx +299 -314
  49. package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
  50. package/src/components/chat/chat-tool-toggles.tsx +26 -17
  51. package/src/components/chat/checkpoint-timeline.tsx +4 -4
  52. package/src/components/chat/message-bubble.tsx +4 -1
  53. package/src/components/chat/message-list.tsx +5 -3
  54. package/src/components/chat/session-debug-panel.tsx +1 -1
  55. package/src/components/chat/tool-request-banner.tsx +3 -3
  56. package/src/components/chatrooms/agent-hover-card.tsx +3 -3
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  59. package/src/components/connectors/connector-list.tsx +265 -127
  60. package/src/components/connectors/connector-sheet.tsx +218 -1
  61. package/src/components/home/home-view.tsx +129 -5
  62. package/src/components/layout/app-layout.tsx +392 -182
  63. package/src/components/layout/mobile-header.tsx +26 -8
  64. package/src/components/plugins/plugin-list.tsx +487 -254
  65. package/src/components/plugins/plugin-sheet.tsx +236 -13
  66. package/src/components/projects/project-detail.tsx +183 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +1 -1
  68. package/src/components/shared/agent-picker-list.tsx +2 -2
  69. package/src/components/shared/command-palette.tsx +111 -25
  70. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  71. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +78 -1
  73. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  74. package/src/components/shared/settings/section-providers.tsx +1 -1
  75. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  76. package/src/components/shared/settings/section-secrets.tsx +6 -6
  77. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  78. package/src/components/shared/settings/section-voice.tsx +5 -1
  79. package/src/components/shared/settings/section-web-search.tsx +10 -2
  80. package/src/components/shared/settings/settings-page.tsx +244 -56
  81. package/src/components/tasks/approvals-panel.tsx +205 -18
  82. package/src/components/tasks/task-board.tsx +242 -46
  83. package/src/components/usage/metrics-dashboard.tsx +147 -1
  84. package/src/components/wallets/wallet-panel.tsx +17 -5
  85. package/src/components/webhooks/webhook-sheet.tsx +8 -8
  86. package/src/lib/auth.ts +17 -0
  87. package/src/lib/chat-streaming-state.test.ts +108 -0
  88. package/src/lib/chat-streaming-state.ts +108 -0
  89. package/src/lib/chat.ts +1 -1
  90. package/src/lib/{sessions.ts → chats.ts} +28 -18
  91. package/src/lib/openclaw-agent-id.test.ts +14 -0
  92. package/src/lib/openclaw-agent-id.ts +31 -0
  93. package/src/lib/providers/claude-cli.ts +1 -1
  94. package/src/lib/server/agent-assignment.test.ts +112 -0
  95. package/src/lib/server/agent-assignment.ts +169 -0
  96. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  97. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  98. package/src/lib/server/approvals.ts +483 -75
  99. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  100. package/src/lib/server/browser-state.test.ts +118 -0
  101. package/src/lib/server/browser-state.ts +123 -0
  102. package/src/lib/server/build-llm.test.ts +36 -0
  103. package/src/lib/server/build-llm.ts +11 -4
  104. package/src/lib/server/builtin-plugins.ts +34 -0
  105. package/src/lib/server/capability-router.ts +10 -8
  106. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  107. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  108. package/src/lib/server/chat-execution.ts +285 -165
  109. package/src/lib/server/chatroom-health.test.ts +26 -0
  110. package/src/lib/server/chatroom-health.ts +2 -3
  111. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  112. package/src/lib/server/chatroom-helpers.ts +48 -8
  113. package/src/lib/server/connectors/discord.ts +175 -11
  114. package/src/lib/server/connectors/doctor.test.ts +80 -0
  115. package/src/lib/server/connectors/doctor.ts +116 -0
  116. package/src/lib/server/connectors/manager.ts +948 -112
  117. package/src/lib/server/connectors/policy.test.ts +222 -0
  118. package/src/lib/server/connectors/policy.ts +452 -0
  119. package/src/lib/server/connectors/slack.ts +188 -9
  120. package/src/lib/server/connectors/telegram.ts +65 -15
  121. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  122. package/src/lib/server/connectors/thread-context.ts +72 -0
  123. package/src/lib/server/connectors/types.ts +41 -11
  124. package/src/lib/server/cost.ts +34 -1
  125. package/src/lib/server/daemon-state.ts +61 -3
  126. package/src/lib/server/data-dir.ts +13 -0
  127. package/src/lib/server/delegation-jobs.test.ts +140 -0
  128. package/src/lib/server/delegation-jobs.ts +248 -0
  129. package/src/lib/server/document-utils.test.ts +47 -0
  130. package/src/lib/server/document-utils.ts +397 -0
  131. package/src/lib/server/heartbeat-service.ts +14 -40
  132. package/src/lib/server/heartbeat-source.test.ts +22 -0
  133. package/src/lib/server/heartbeat-source.ts +7 -0
  134. package/src/lib/server/identity-continuity.test.ts +77 -0
  135. package/src/lib/server/identity-continuity.ts +127 -0
  136. package/src/lib/server/mailbox-utils.ts +347 -0
  137. package/src/lib/server/main-agent-loop.ts +28 -1103
  138. package/src/lib/server/memory-db.ts +4 -6
  139. package/src/lib/server/memory-tiers.ts +40 -0
  140. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  141. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  142. package/src/lib/server/openclaw-exec-config.ts +5 -6
  143. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  144. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  145. package/src/lib/server/openclaw-sync.ts +3 -2
  146. package/src/lib/server/orchestrator-lg.ts +20 -9
  147. package/src/lib/server/orchestrator.ts +7 -7
  148. package/src/lib/server/playwright-proxy.mjs +27 -3
  149. package/src/lib/server/plugins.test.ts +207 -0
  150. package/src/lib/server/plugins.ts +927 -66
  151. package/src/lib/server/provider-health.ts +38 -6
  152. package/src/lib/server/queue.ts +13 -28
  153. package/src/lib/server/scheduler.ts +2 -0
  154. package/src/lib/server/session-archive-memory.test.ts +85 -0
  155. package/src/lib/server/session-archive-memory.ts +230 -0
  156. package/src/lib/server/session-mailbox.ts +8 -18
  157. package/src/lib/server/session-reset-policy.test.ts +99 -0
  158. package/src/lib/server/session-reset-policy.ts +311 -0
  159. package/src/lib/server/session-run-manager.ts +33 -82
  160. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  161. package/src/lib/server/session-tools/calendar.ts +366 -0
  162. package/src/lib/server/session-tools/canvas.ts +1 -1
  163. package/src/lib/server/session-tools/chatroom.ts +4 -2
  164. package/src/lib/server/session-tools/connector.ts +114 -10
  165. package/src/lib/server/session-tools/context.ts +21 -5
  166. package/src/lib/server/session-tools/crawl.ts +447 -0
  167. package/src/lib/server/session-tools/crud.ts +74 -28
  168. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  169. package/src/lib/server/session-tools/delegate.ts +497 -24
  170. package/src/lib/server/session-tools/discovery.ts +24 -6
  171. package/src/lib/server/session-tools/document.ts +283 -0
  172. package/src/lib/server/session-tools/edit_file.ts +4 -2
  173. package/src/lib/server/session-tools/email.ts +320 -0
  174. package/src/lib/server/session-tools/extract.ts +137 -0
  175. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  176. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  177. package/src/lib/server/session-tools/file.ts +241 -25
  178. package/src/lib/server/session-tools/git.ts +1 -1
  179. package/src/lib/server/session-tools/http.ts +1 -1
  180. package/src/lib/server/session-tools/human-loop.ts +227 -0
  181. package/src/lib/server/session-tools/image-gen.ts +380 -0
  182. package/src/lib/server/session-tools/index.ts +130 -50
  183. package/src/lib/server/session-tools/mailbox.ts +276 -0
  184. package/src/lib/server/session-tools/memory.ts +172 -3
  185. package/src/lib/server/session-tools/monitor.ts +151 -8
  186. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  187. package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
  188. package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
  189. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  190. package/src/lib/server/session-tools/platform.ts +148 -7
  191. package/src/lib/server/session-tools/plugin-creator.ts +89 -26
  192. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  193. package/src/lib/server/session-tools/replicate.ts +301 -0
  194. package/src/lib/server/session-tools/sample-ui.ts +1 -1
  195. package/src/lib/server/session-tools/sandbox.ts +4 -2
  196. package/src/lib/server/session-tools/schedule.ts +24 -12
  197. package/src/lib/server/session-tools/session-info.ts +43 -7
  198. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  199. package/src/lib/server/session-tools/shell.ts +5 -2
  200. package/src/lib/server/session-tools/subagent.ts +194 -28
  201. package/src/lib/server/session-tools/table.ts +587 -0
  202. package/src/lib/server/session-tools/wallet.ts +42 -12
  203. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  204. package/src/lib/server/session-tools/web.ts +926 -91
  205. package/src/lib/server/storage.ts +255 -16
  206. package/src/lib/server/stream-agent-chat.ts +116 -268
  207. package/src/lib/server/structured-extract.test.ts +72 -0
  208. package/src/lib/server/structured-extract.ts +373 -0
  209. package/src/lib/server/task-mention.test.ts +16 -2
  210. package/src/lib/server/task-mention.ts +61 -10
  211. package/src/lib/server/tool-aliases.ts +66 -18
  212. package/src/lib/server/tool-capability-policy.test.ts +9 -9
  213. package/src/lib/server/tool-capability-policy.ts +38 -27
  214. package/src/lib/server/tool-retry.ts +2 -0
  215. package/src/lib/server/watch-jobs.test.ts +173 -0
  216. package/src/lib/server/watch-jobs.ts +532 -0
  217. package/src/lib/server/ws-hub.ts +5 -3
  218. package/src/lib/tool-definitions.ts +4 -0
  219. package/src/lib/validation/schemas.test.ts +26 -0
  220. package/src/lib/validation/schemas.ts +10 -1
  221. package/src/lib/ws-client.ts +14 -12
  222. package/src/proxy.ts +5 -5
  223. package/src/stores/use-app-store.ts +5 -11
  224. package/src/stores/use-chat-store.ts +38 -9
  225. package/src/types/index.ts +352 -47
  226. package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
  227. package/src/components/sessions/new-session-sheet.tsx +0 -253
  228. package/src/lib/server/main-session.ts +0 -24
  229. package/src/lib/server/session-run-manager.test.ts +0 -23
  230. /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
  231. /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
  232. /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
  233. /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
  234. /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
  235. /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
  236. /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
  237. /package/src/app/api/{sessions → chats}/heartbeat/route.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { useCallback, useEffect, useMemo } from 'react'
3
+ import { useCallback, useEffect, useMemo, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { useApprovalStore } from '@/stores/use-approval-store'
6
6
  import { api } from '@/lib/api-client'
@@ -26,6 +26,16 @@ const CATEGORY_ICONS: Record<string, string> = {
26
26
  task_tool: '🤖',
27
27
  }
28
28
 
29
+ type ApprovalScope = 'all' | 'execution' | 'workflow' | 'task'
30
+
31
+ function relativeTime(ts: number): string {
32
+ const diff = Date.now() - ts
33
+ if (diff < 60_000) return 'just now'
34
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
35
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
36
+ return `${Math.floor(diff / 86_400_000)}d ago`
37
+ }
38
+
29
39
  export function ApprovalsPanel() {
30
40
  const tasks = useAppStore((s) => s.tasks)
31
41
  const agents = useAppStore((s) => s.agents)
@@ -59,6 +69,10 @@ export function ApprovalsPanel() {
59
69
  useWs('approvals', refreshServerApprovals, 5000)
60
70
  useWs('openclaw:approvals', refreshExecApprovals, 5000)
61
71
 
72
+ const [search, setSearch] = useState('')
73
+ const [scope, setScope] = useState<ApprovalScope>('all')
74
+ const [categoryFilter, setCategoryFilter] = useState('all')
75
+
62
76
  const taskApprovals = useMemo(() => {
63
77
  return Object.values(tasks)
64
78
  .filter((t) => t.pendingApproval)
@@ -92,6 +106,73 @@ export function ApprovalsPanel() {
92
106
  }, [execApprovals])
93
107
 
94
108
  const pendingCount = sortedExecApprovals.length + workflowApprovals.length
109
+ const searchTerm = search.trim().toLowerCase()
110
+
111
+ const workflowCategories = useMemo(() => (
112
+ Array.from(new Set(workflowApprovals.map((req) => req.category))).sort()
113
+ ), [workflowApprovals])
114
+
115
+ const filteredExecApprovals = useMemo(() => {
116
+ if (scope === 'workflow' || scope === 'task') return []
117
+ return sortedExecApprovals.filter((approval) => {
118
+ if (!searchTerm) return true
119
+ return [
120
+ approval.ask,
121
+ approval.command,
122
+ approval.cwd,
123
+ approval.host,
124
+ approval.security,
125
+ ].some((value) => value?.toLowerCase().includes(searchTerm))
126
+ })
127
+ }, [scope, sortedExecApprovals, searchTerm])
128
+
129
+ const filteredWorkflowApprovals = useMemo(() => {
130
+ return workflowApprovals.filter((req) => {
131
+ if (scope === 'execution') return false
132
+ if (scope === 'workflow' && req.category === 'task_tool') return false
133
+ if (scope === 'task' && req.category !== 'task_tool') return false
134
+ if (categoryFilter !== 'all' && req.category !== categoryFilter) return false
135
+ if (!searchTerm) return true
136
+ const agentName = req.agentId ? agents[req.agentId]?.name : 'system'
137
+ const payloadText = JSON.stringify(getApprovalPayload(req))
138
+ return [
139
+ getApprovalTitle(req),
140
+ req.description,
141
+ req.category,
142
+ agentName,
143
+ payloadText,
144
+ ].some((value) => value?.toLowerCase().includes(searchTerm))
145
+ })
146
+ }, [agents, categoryFilter, scope, searchTerm, workflowApprovals])
147
+
148
+ const visibleCount = filteredExecApprovals.length + filteredWorkflowApprovals.length
149
+
150
+ const summaryCards = [
151
+ {
152
+ label: 'Execution',
153
+ value: sortedExecApprovals.length,
154
+ tone: 'text-amber-400',
155
+ hint: 'Command approvals from OpenClaw',
156
+ },
157
+ {
158
+ label: 'Workflow',
159
+ value: sessionApprovals.length,
160
+ tone: 'text-sky-400',
161
+ hint: 'Agent and plugin governance requests',
162
+ },
163
+ {
164
+ label: 'Task Calls',
165
+ value: taskApprovals.length,
166
+ tone: 'text-violet-400',
167
+ hint: 'Tasks waiting on tool approval',
168
+ },
169
+ {
170
+ label: 'Recently Active',
171
+ value: workflowApprovals.filter((req) => Date.now() - req.updatedAt < 60 * 60 * 1000).length,
172
+ tone: 'text-emerald-400',
173
+ hint: 'Updated in the last hour',
174
+ },
175
+ ]
95
176
 
96
177
  const handleDecision = async (req: ApprovalRequest, approved: boolean) => {
97
178
  try {
@@ -131,29 +212,98 @@ export function ApprovalsPanel() {
131
212
  <div className="flex items-center justify-between mb-8">
132
213
  <div>
133
214
  <h1 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-1">Approvals</h1>
134
- <p className="text-[13px] text-text-3">Execution and plugin governance requests pending review</p>
215
+ <p className="text-[13px] text-text-3">Execution, task, and governance requests queued for review</p>
135
216
  </div>
136
217
  <div className="px-3 py-1.5 rounded-full bg-amber-500/10 border border-amber-500/20 text-amber-400 text-[11px] font-600">
137
218
  {pendingCount} Pending
138
219
  </div>
139
220
  </div>
140
221
 
141
- {sortedExecApprovals.length > 0 && (
222
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
223
+ {summaryCards.map((card) => (
224
+ <div key={card.label} className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-3.5">
225
+ <div className={`text-[22px] font-display font-700 tracking-[-0.03em] ${card.tone}`}>
226
+ {card.value}
227
+ </div>
228
+ <div className="text-[11px] font-600 text-text-2 mt-0.5">{card.label}</div>
229
+ <p className="text-[10px] text-text-3/50 mt-1 leading-relaxed">{card.hint}</p>
230
+ </div>
231
+ ))}
232
+ </div>
233
+
234
+ <div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4 mb-6">
235
+ <div className="flex flex-col lg:flex-row gap-3 lg:items-center lg:justify-between">
236
+ <div className="flex flex-wrap gap-2">
237
+ {([
238
+ ['all', `All (${pendingCount})`],
239
+ ['execution', `Execution (${sortedExecApprovals.length})`],
240
+ ['workflow', `Workflow (${sessionApprovals.length})`],
241
+ ['task', `Tasks (${taskApprovals.length})`],
242
+ ] as const).map(([value, label]) => (
243
+ <button
244
+ key={value}
245
+ onClick={() => setScope(value)}
246
+ className={`px-3 py-1.5 rounded-[9px] text-[11px] font-600 transition-all cursor-pointer border-none ${
247
+ scope === value
248
+ ? 'bg-accent-soft text-accent-bright'
249
+ : 'bg-white/[0.04] text-text-3 hover:bg-white/[0.08] hover:text-text-2'
250
+ }`}
251
+ style={{ fontFamily: 'inherit' }}
252
+ >
253
+ {label}
254
+ </button>
255
+ ))}
256
+ </div>
257
+
258
+ <div className="flex items-center gap-2">
259
+ <div className="text-[11px] text-text-3/60 font-600">
260
+ Showing {visibleCount} of {pendingCount}
261
+ </div>
262
+ {workflowCategories.length > 1 && scope !== 'execution' && (
263
+ <select
264
+ value={categoryFilter}
265
+ onChange={(e) => setCategoryFilter(e.target.value)}
266
+ className="px-3 py-2 rounded-[10px] bg-white/[0.04] border border-white/[0.06] text-[12px] text-text-2 outline-none"
267
+ style={{ fontFamily: 'inherit' }}
268
+ >
269
+ <option value="all">All categories</option>
270
+ {workflowCategories.map((category) => (
271
+ <option key={category} value={category}>
272
+ {CATEGORY_LABELS[category] || category}
273
+ </option>
274
+ ))}
275
+ </select>
276
+ )}
277
+ </div>
278
+ </div>
279
+
280
+ <div className="mt-3">
281
+ <input
282
+ value={search}
283
+ onChange={(e) => setSearch(e.target.value)}
284
+ placeholder="Search approvals by agent, tool, command, or payload"
285
+ className="w-full px-4 py-2.5 rounded-[12px] border border-white/[0.06] bg-surface text-[13px] text-text placeholder:text-text-3/50 outline-none focus:border-white/[0.12]"
286
+ style={{ fontFamily: 'inherit' }}
287
+ />
288
+ </div>
289
+ </div>
290
+
291
+ {filteredExecApprovals.length > 0 && (
142
292
  <div className="mb-6">
143
293
  <h2 className="text-[12px] font-700 uppercase tracking-[0.1em] text-amber-400/90 mb-2">Execution Approvals</h2>
144
294
  <div className="grid grid-cols-1 gap-3">
145
- {sortedExecApprovals.map((approval) => (
295
+ {filteredExecApprovals.map((approval) => (
146
296
  <ExecApprovalCard key={approval.id} approval={approval} />
147
297
  ))}
148
298
  </div>
149
299
  </div>
150
300
  )}
151
301
 
152
- {workflowApprovals.length > 0 && (
302
+ {filteredWorkflowApprovals.length > 0 && (
153
303
  <div>
154
- <h2 className="text-[12px] font-700 uppercase tracking-[0.1em] text-amber-400/90 mb-2">Plugin Workflow Approvals</h2>
304
+ <h2 className="text-[12px] font-700 uppercase tracking-[0.1em] text-amber-400/90 mb-2">Workflow Approvals</h2>
155
305
  <div className="grid grid-cols-1 gap-4">
156
- {workflowApprovals.map((req) => {
306
+ {filteredWorkflowApprovals.map((req) => {
157
307
  const agent = req.agentId ? agents[req.agentId] : null
158
308
  const icon = CATEGORY_ICONS[req.category] || '⚠️'
159
309
  const categoryLabel = CATEGORY_LABELS[req.category] || req.category
@@ -173,10 +323,23 @@ export function ApprovalsPanel() {
173
323
  <span className="px-1.5 py-0.5 rounded-[4px] bg-white/[0.04] text-[9px] font-600 text-text-3/60 uppercase tracking-wider">
174
324
  {categoryLabel}
175
325
  </span>
326
+ {req.taskId && (
327
+ <span className="px-1.5 py-0.5 rounded-[4px] bg-violet-500/10 text-[9px] font-700 text-violet-300 uppercase tracking-wider">
328
+ Task
329
+ </span>
330
+ )}
331
+ </div>
332
+ <div className="flex flex-wrap items-center gap-x-2 gap-y-1 mt-1 text-[11px] text-text-3">
333
+ <span>{agent?.name || 'System'}</span>
334
+ <span className="text-text-3/35">•</span>
335
+ <span>{relativeTime(req.updatedAt)}</span>
336
+ {req.description && (
337
+ <>
338
+ <span className="text-text-3/35">•</span>
339
+ <span className="truncate max-w-[280px]">{req.description}</span>
340
+ </>
341
+ )}
176
342
  </div>
177
- <p className="text-[11px] text-text-3">
178
- {agent?.name || 'System'}
179
- </p>
180
343
  </div>
181
344
  </div>
182
345
  <span className="text-[10px] text-text-3/50 font-mono">
@@ -185,16 +348,33 @@ export function ApprovalsPanel() {
185
348
  </div>
186
349
 
187
350
  <div className="p-5">
188
- {req.description && (
189
- <p className="text-[13px] text-text-2/90 mb-4">{req.description}</p>
190
- )}
191
-
192
- <div className="bg-black/30 rounded-[10px] border border-white/[0.04] p-4 mb-5 overflow-x-auto max-h-[250px] overflow-y-auto">
193
- <pre className="text-[12px] font-mono text-text-2/80 whitespace-pre-wrap break-all leading-relaxed">
194
- {payloadText === '{}' ? 'No structured payload provided.' : payloadText}
195
- </pre>
351
+ <div className="flex flex-wrap gap-2 mb-4">
352
+ {req.sessionId && (
353
+ <span className="px-2 py-1 rounded-[7px] bg-white/[0.04] text-[10px] font-600 text-text-3">
354
+ Session {req.sessionId.slice(0, 8)}
355
+ </span>
356
+ )}
357
+ {req.taskId && (
358
+ <span className="px-2 py-1 rounded-[7px] bg-violet-500/10 text-[10px] font-600 text-violet-300">
359
+ Task {req.taskId.slice(0, 8)}
360
+ </span>
361
+ )}
196
362
  </div>
197
363
 
364
+ <details className="mb-5 rounded-[10px] border border-white/[0.04] bg-black/20 overflow-hidden group">
365
+ <summary className="list-none cursor-pointer px-4 py-3 flex items-center justify-between text-[12px] font-600 text-text-2">
366
+ <span>Payload details</span>
367
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="text-text-3 transition-transform group-open:rotate-180">
368
+ <polyline points="6 9 12 15 18 9" />
369
+ </svg>
370
+ </summary>
371
+ <div className="px-4 pb-4 overflow-x-auto max-h-[260px] overflow-y-auto">
372
+ <pre className="text-[12px] font-mono text-text-2/80 whitespace-pre-wrap break-all leading-relaxed">
373
+ {payloadText === '{}' ? 'No structured payload provided.' : payloadText}
374
+ </pre>
375
+ </div>
376
+ </details>
377
+
198
378
  <div className="flex items-center justify-end gap-3 pt-4 border-t border-white/[0.04]">
199
379
  <button
200
380
  onClick={() => handleDecision(req, false)}
@@ -218,6 +398,13 @@ export function ApprovalsPanel() {
218
398
  </div>
219
399
  </div>
220
400
  )}
401
+
402
+ {visibleCount === 0 && pendingCount > 0 && (
403
+ <div className="rounded-[16px] border border-dashed border-white/[0.08] px-6 py-10 text-center">
404
+ <p className="text-[13px] font-600 text-text-2 mb-1">No approvals match the current filters</p>
405
+ <p className="text-[12px] text-text-3/60">Try clearing the search or switching the queue scope.</p>
406
+ </div>
407
+ )}
221
408
  </div>
222
409
  </div>
223
410
  )
@@ -5,12 +5,42 @@ import { useAppStore } from '@/stores/use-app-store'
5
5
  import { useWs } from '@/hooks/use-ws'
6
6
  import { updateTask, bulkUpdateTasks } from '@/lib/tasks'
7
7
  import { TaskColumn } from './task-column'
8
+ import { TaskCard } from './task-card'
8
9
  import { Skeleton } from '@/components/shared/skeleton'
9
10
  import { AgentAvatar } from '@/components/agents/agent-avatar'
10
- import type { BoardTaskStatus } from '@/types'
11
+ import type { BoardTask, BoardTaskStatus } from '@/types'
11
12
  import { toast } from 'sonner'
12
13
 
13
14
  const ACTIVE_COLUMNS: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed']
15
+ type BoardViewMode = 'board' | 'list'
16
+ type AttentionFilter = 'all' | 'needs-attention' | 'approval' | 'blocked' | 'overdue' | 'failed'
17
+
18
+ function isTaskOverdue(task: BoardTask): boolean {
19
+ return !!task.dueAt && task.dueAt < Date.now() && task.status !== 'completed' && task.status !== 'archived'
20
+ }
21
+
22
+ function matchesAttentionFilter(task: BoardTask, filter: AttentionFilter): boolean {
23
+ const blocked = !!task.blockedBy?.length
24
+ const pendingApproval = !!task.pendingApproval
25
+ const overdue = isTaskOverdue(task)
26
+ const failed = task.status === 'failed'
27
+ if (filter === 'all') return true
28
+ if (filter === 'approval') return pendingApproval
29
+ if (filter === 'blocked') return blocked
30
+ if (filter === 'overdue') return overdue
31
+ if (filter === 'failed') return failed
32
+ return blocked || pendingApproval || overdue || failed
33
+ }
34
+
35
+ function attentionRank(task: BoardTask): number {
36
+ if (task.pendingApproval) return 0
37
+ if (task.status === 'failed') return 1
38
+ if (task.blockedBy?.length) return 2
39
+ if (isTaskOverdue(task)) return 3
40
+ if (task.status === 'running') return 4
41
+ if (task.status === 'queued') return 5
42
+ return 6
43
+ }
14
44
 
15
45
  export function TaskBoard() {
16
46
  const tasks = useAppStore((s) => s.tasks)
@@ -41,17 +71,6 @@ export function TaskBoard() {
41
71
 
42
72
  const clearSelection = useCallback(() => setSelectedIds(new Set()), [])
43
73
 
44
- const selectAllInColumn = useCallback((status: BoardTaskStatus) => {
45
- const ids = Object.values(tasks)
46
- .filter((t) => t.status === status)
47
- .map((t) => t.id)
48
- setSelectedIds((prev) => {
49
- const next = new Set(prev)
50
- ids.forEach((id) => next.add(id))
51
- return next
52
- })
53
- }, [tasks])
54
-
55
74
  // Bulk action handlers
56
75
  const [bulkActing, setBulkActing] = useState(false)
57
76
  const handleBulkStatus = useCallback(async (status: BoardTaskStatus) => {
@@ -120,6 +139,8 @@ export function TaskBoard() {
120
139
  if (typeof window === 'undefined') return ''
121
140
  return new URLSearchParams(window.location.search).get('tag') || ''
122
141
  })
142
+ const [viewMode, setViewMode] = useState<BoardViewMode>('board')
143
+ const [attentionFilter, setAttentionFilter] = useState<AttentionFilter>('all')
123
144
 
124
145
  // Seed activeProjectFilter from URL on mount
125
146
  useEffect(() => {
@@ -152,14 +173,43 @@ export function TaskBoard() {
152
173
 
153
174
  const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
154
175
 
155
- const tasksByStatus = useCallback((status: BoardTaskStatus) =>
176
+ const matchesBaseFilters = useCallback((task: BoardTask) => {
177
+ if (!showArchived && task.status === 'archived') return false
178
+ if (filterAgentId && task.agentId !== filterAgentId) return false
179
+ if (filterTag && !(task.tags && task.tags.includes(filterTag))) return false
180
+ if (activeProjectFilter && task.projectId !== activeProjectFilter) return false
181
+ if (!matchesAttentionFilter(task, attentionFilter)) return false
182
+ return true
183
+ }, [activeProjectFilter, attentionFilter, filterAgentId, filterTag, showArchived])
184
+
185
+ const filteredTasks = useMemo(() => (
156
186
  Object.values(tasks)
157
- .filter((t) => t.status === status
158
- && (!filterAgentId || t.agentId === filterAgentId)
159
- && (!filterTag || (t.tags && t.tags.includes(filterTag)))
160
- && (!activeProjectFilter || t.projectId === activeProjectFilter))
187
+ .filter(matchesBaseFilters)
188
+ .sort((a, b) => {
189
+ const rankDiff = attentionRank(a) - attentionRank(b)
190
+ if (rankDiff !== 0) return rankDiff
191
+ const dueDiff = (a.dueAt || Number.MAX_SAFE_INTEGER) - (b.dueAt || Number.MAX_SAFE_INTEGER)
192
+ if (dueDiff !== 0) return dueDiff
193
+ return b.updatedAt - a.updatedAt
194
+ })
195
+ ), [tasks, matchesBaseFilters])
196
+
197
+ const tasksByStatus = useCallback((status: BoardTaskStatus) =>
198
+ filteredTasks
199
+ .filter((t) => t.status === status)
161
200
  .sort((a, b) => b.updatedAt - a.updatedAt),
162
- [tasks, filterAgentId, filterTag, activeProjectFilter])
201
+ [filteredTasks])
202
+
203
+ const selectAllInColumn = useCallback((status: BoardTaskStatus) => {
204
+ const ids = filteredTasks
205
+ .filter((t) => t.status === status)
206
+ .map((t) => t.id)
207
+ setSelectedIds((prev) => {
208
+ const next = new Set(prev)
209
+ ids.forEach((id) => next.add(id))
210
+ return next
211
+ })
212
+ }, [filteredTasks])
163
213
 
164
214
  const handleDrop = useCallback(async (taskId: string, newStatus: BoardTaskStatus) => {
165
215
  const task = tasks[taskId]
@@ -187,10 +237,22 @@ export function TaskBoard() {
187
237
  running: all.filter((t) => t.status === 'running').length,
188
238
  completed: all.filter((t) => t.status === 'completed').length,
189
239
  failed: all.filter((t) => t.status === 'failed').length,
190
- overdue: all.filter((t) => t.dueAt && t.dueAt < Date.now() && t.status !== 'completed').length,
240
+ overdue: all.filter((t) => isTaskOverdue(t)).length,
241
+ blocked: all.filter((t) => (t.blockedBy?.length || 0) > 0).length,
242
+ approvals: all.filter((t) => !!t.pendingApproval).length,
243
+ attention: all.filter((t) => matchesAttentionFilter(t, 'needs-attention')).length,
191
244
  }
192
245
  }, [tasks])
193
246
 
247
+ const activeAttentionLabel = useMemo(() => {
248
+ if (attentionFilter === 'all') return null
249
+ if (attentionFilter === 'needs-attention') return 'Needs attention'
250
+ if (attentionFilter === 'approval') return 'Awaiting approval'
251
+ if (attentionFilter === 'blocked') return 'Blocked tasks'
252
+ if (attentionFilter === 'overdue') return 'Overdue tasks'
253
+ return 'Failed tasks'
254
+ }, [attentionFilter])
255
+
194
256
  // Custom dropdown state
195
257
  const [projectDropdownOpen, setProjectDropdownOpen] = useState(false)
196
258
  const projectDropdownRef = useRef<HTMLDivElement>(null)
@@ -253,6 +315,25 @@ export function TaskBoard() {
253
315
  </div>
254
316
  </div>
255
317
  <div className="flex items-center gap-3">
318
+ <div className="flex items-center gap-1 p-1 rounded-[11px] bg-surface-2 border border-white/[0.06]">
319
+ {([
320
+ ['board', 'Board'],
321
+ ['list', 'List'],
322
+ ] as const).map(([value, label]) => (
323
+ <button
324
+ key={value}
325
+ onClick={() => setViewMode(value)}
326
+ className={`px-3 py-1.5 rounded-[8px] text-[12px] font-700 transition-all cursor-pointer border-none ${
327
+ viewMode === value
328
+ ? 'bg-accent-soft text-accent-bright'
329
+ : 'text-text-3 hover:text-text-2'
330
+ }`}
331
+ style={{ fontFamily: 'inherit' }}
332
+ >
333
+ {label}
334
+ </button>
335
+ ))}
336
+ </div>
256
337
  <div className="relative" ref={agentDropdownRef}>
257
338
  <button
258
339
  onClick={() => setAgentDropdownOpen(!agentDropdownOpen)}
@@ -384,8 +465,64 @@ export function TaskBoard() {
384
465
  </div>
385
466
  </div>
386
467
 
387
- {activeProjectFilter && projects[activeProjectFilter] && (
388
- <div className="flex items-center gap-2 px-8 pb-3">
468
+ <div className="grid grid-cols-2 lg:grid-cols-5 gap-3 px-8 pb-4">
469
+ {[
470
+ { key: 'needs-attention', label: 'Needs Attention', value: stats.attention, tone: 'text-red-300', accent: 'bg-red-500/10' },
471
+ { key: 'approval', label: 'Approvals', value: stats.approvals, tone: 'text-amber-400', accent: 'bg-amber-500/10' },
472
+ { key: 'blocked', label: 'Blocked', value: stats.blocked, tone: 'text-rose-400', accent: 'bg-rose-500/10' },
473
+ { key: 'overdue', label: 'Overdue', value: stats.overdue, tone: 'text-red-400', accent: 'bg-red-500/10' },
474
+ { key: 'failed', label: 'Failed', value: stats.failed, tone: 'text-orange-400', accent: 'bg-orange-500/10' },
475
+ ].map((item) => (
476
+ <button
477
+ key={item.key}
478
+ onClick={() => setAttentionFilter((current) => (current === item.key ? 'all' : item.key as AttentionFilter))}
479
+ className={`rounded-[14px] border px-4 py-3 text-left transition-all cursor-pointer ${
480
+ attentionFilter === item.key
481
+ ? 'border-white/[0.12] bg-white/[0.05]'
482
+ : 'border-white/[0.06] bg-white/[0.02] hover:bg-white/[0.04]'
483
+ }`}
484
+ style={{ fontFamily: 'inherit' }}
485
+ >
486
+ <div className={`inline-flex items-center rounded-full px-2 py-1 text-[10px] font-700 uppercase tracking-[0.08em] ${item.accent} ${item.tone}`}>
487
+ {item.label}
488
+ </div>
489
+ <div className={`mt-3 text-[24px] font-display font-700 tracking-[-0.03em] ${item.tone}`}>
490
+ {item.value}
491
+ </div>
492
+ <p className="mt-1 text-[11px] text-text-3/60">
493
+ {item.value === 0 ? 'Nothing waiting here' : 'Click to focus this queue'}
494
+ </p>
495
+ </button>
496
+ ))}
497
+ </div>
498
+
499
+ <div className="flex flex-wrap items-center gap-2 px-8 pb-3">
500
+ {([
501
+ ['all', 'All'],
502
+ ['needs-attention', 'Needs Attention'],
503
+ ['approval', 'Awaiting Approval'],
504
+ ['blocked', 'Blocked'],
505
+ ['overdue', 'Overdue'],
506
+ ['failed', 'Failed'],
507
+ ] as const).map(([value, label]) => (
508
+ <button
509
+ key={value}
510
+ onClick={() => setAttentionFilter(value)}
511
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 transition-all cursor-pointer border-none ${
512
+ attentionFilter === value
513
+ ? 'bg-accent-soft text-accent-bright'
514
+ : 'bg-white/[0.04] text-text-3 hover:bg-white/[0.08] hover:text-text-2'
515
+ }`}
516
+ style={{ fontFamily: 'inherit' }}
517
+ >
518
+ {label}
519
+ </button>
520
+ ))}
521
+ </div>
522
+
523
+ {(activeProjectFilter && projects[activeProjectFilter]) || activeAttentionLabel ? (
524
+ <div className="flex flex-wrap items-center gap-2 px-8 pb-3">
525
+ {activeProjectFilter && projects[activeProjectFilter] && (
389
526
  <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">
390
527
  <span className="w-2 h-2 rounded-full" style={{ backgroundColor: projects[activeProjectFilter].color || '#6366F1' }} />
391
528
  {projects[activeProjectFilter].name}
@@ -396,11 +533,24 @@ export function TaskBoard() {
396
533
  &times;
397
534
  </button>
398
535
  </span>
536
+ )}
537
+ {activeAttentionLabel && (
538
+ <span className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] bg-amber-500/10 border border-amber-500/20 text-[12px] font-600 text-amber-400">
539
+ {activeAttentionLabel}
540
+ <button
541
+ onClick={() => setAttentionFilter('all')}
542
+ className="ml-1 text-amber-300 hover:text-white cursor-pointer border-none bg-transparent p-0 text-[14px] leading-none"
543
+ >
544
+ &times;
545
+ </button>
546
+ </span>
547
+ )}
399
548
  </div>
400
- )}
549
+ ) : null}
401
550
 
402
- <div className="flex-1 min-h-0 flex gap-5 px-8 pb-6 overflow-x-auto overflow-y-hidden overscroll-x-contain touch-pan-x">
403
- {!loaded ? (
551
+ {viewMode === 'board' ? (
552
+ <div className="flex-1 min-h-0 flex gap-5 px-8 pb-6 overflow-x-auto overflow-y-hidden overscroll-x-contain touch-pan-x">
553
+ {!loaded ? (
404
554
  ACTIVE_COLUMNS.map((status) => (
405
555
  <div key={status} className="flex flex-col gap-3 min-w-[260px] flex-1">
406
556
  <Skeleton className="rounded-[10px]" width="100%" height={32} />
@@ -409,29 +559,75 @@ export function TaskBoard() {
409
559
  ))}
410
560
  </div>
411
561
  ))
412
- ) : (
413
- columns.map((status, idx) => (
414
- <div
415
- key={status}
416
- className="flex flex-col gap-3 min-w-[260px] flex-1"
417
- style={{
418
- animation: 'fade-up 0.6s var(--ease-spring) both',
419
- animationDelay: `${idx * 0.1}s`
420
- }}
421
- >
422
- <TaskColumn
423
- status={status}
424
- tasks={tasksByStatus(status)}
425
- onDrop={handleDrop}
426
- selectionMode={selectionMode}
427
- selectedIds={selectedIds}
428
- onToggleSelect={toggleSelect}
429
- onSelectAll={() => selectAllInColumn(status)}
430
- />
562
+ ) : (
563
+ columns.map((status, idx) => (
564
+ <div
565
+ key={status}
566
+ className="flex flex-col gap-3 min-w-[260px] flex-1"
567
+ style={{
568
+ animation: 'fade-up 0.6s var(--ease-spring) both',
569
+ animationDelay: `${idx * 0.1}s`
570
+ }}
571
+ >
572
+ <TaskColumn
573
+ status={status}
574
+ tasks={tasksByStatus(status)}
575
+ onDrop={handleDrop}
576
+ selectionMode={selectionMode}
577
+ selectedIds={selectedIds}
578
+ onToggleSelect={toggleSelect}
579
+ onSelectAll={() => selectAllInColumn(status)}
580
+ />
581
+ </div>
582
+ ))
583
+ )}
584
+ </div>
585
+ ) : (
586
+ <div className="flex-1 overflow-y-auto px-8 pb-6">
587
+ {!loaded ? (
588
+ <div className="max-w-4xl mx-auto flex flex-col gap-3">
589
+ {Array.from({ length: 6 }).map((_, i) => (
590
+ <Skeleton key={i} className="rounded-[14px]" width="100%" height={112} />
591
+ ))}
431
592
  </div>
432
- ))
433
- )}
434
- </div>
593
+ ) : filteredTasks.length === 0 ? (
594
+ <div className="max-w-3xl mx-auto rounded-[16px] border border-dashed border-white/[0.08] px-6 py-14 text-center">
595
+ <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>
597
+ </div>
598
+ ) : (
599
+ <div className="max-w-4xl mx-auto">
600
+ <div className="flex items-center justify-between mb-4">
601
+ <div>
602
+ <h2 className="font-display text-[18px] font-700 tracking-[-0.02em] text-text">
603
+ {attentionFilter === 'all' ? 'Task List' : activeAttentionLabel || 'Task List'}
604
+ </h2>
605
+ <p className="text-[12px] text-text-3/60">
606
+ {attentionFilter === 'all'
607
+ ? 'All visible tasks, sorted by urgency and freshness.'
608
+ : 'Sorted by approval, failures, blockers, and due dates.'}
609
+ </p>
610
+ </div>
611
+ <div className="text-[12px] text-text-3/60">
612
+ {filteredTasks.length} visible task{filteredTasks.length !== 1 ? 's' : ''}
613
+ </div>
614
+ </div>
615
+ <div className="flex flex-col gap-3">
616
+ {filteredTasks.map((task, idx) => (
617
+ <TaskCard
618
+ key={task.id}
619
+ task={task}
620
+ index={idx}
621
+ selectionMode={selectionMode}
622
+ selected={selectedIds.has(task.id)}
623
+ onToggleSelect={toggleSelect}
624
+ />
625
+ ))}
626
+ </div>
627
+ </div>
628
+ )}
629
+ </div>
630
+ )}
435
631
 
436
632
  {/* Bulk action bar */}
437
633
  {selectionMode && (