@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.
- package/README.md +155 -150
- package/package.json +1 -1
- package/src/app/api/agents/[id]/route.ts +26 -0
- package/src/app/api/agents/[id]/thread/route.ts +37 -9
- package/src/app/api/agents/route.ts +13 -2
- package/src/app/api/auth/route.ts +76 -7
- package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
- package/src/app/api/{sessions → chats}/[id]/browser/route.ts +5 -1
- package/src/app/api/{sessions → chats}/[id]/chat/route.ts +7 -3
- package/src/app/api/{sessions → chats}/[id]/checkpoints/route.ts +1 -1
- package/src/app/api/chats/[id]/main-loop/route.ts +13 -0
- package/src/app/api/{sessions → chats}/[id]/messages/route.ts +19 -13
- package/src/app/api/{sessions → chats}/[id]/restore/route.ts +1 -1
- package/src/app/api/{sessions → chats}/[id]/route.ts +22 -52
- package/src/app/api/{sessions → chats}/[id]/stop/route.ts +6 -1
- package/src/app/api/{sessions → chats}/route.ts +21 -7
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/skills/route.ts +11 -3
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +6 -26
- package/src/app/api/plugins/settings/route.ts +40 -0
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/settings/route.ts +49 -7
- package/src/app/api/tasks/[id]/route.ts +15 -6
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/usage/route.ts +30 -0
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +9 -2
- package/src/cli/index.js +39 -33
- package/src/cli/index.ts +43 -49
- package/src/cli/spec.js +29 -27
- package/src/components/agents/agent-card.tsx +16 -13
- package/src/components/agents/agent-chat-list.tsx +104 -4
- package/src/components/agents/agent-list.tsx +54 -22
- package/src/components/agents/agent-sheet.tsx +209 -18
- package/src/components/agents/cron-job-form.tsx +3 -3
- package/src/components/agents/inspector-panel.tsx +110 -50
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +5 -38
- package/src/components/chat/chat-area.tsx +39 -27
- package/src/components/{sessions/session-card.tsx → chat/chat-card.tsx} +7 -23
- package/src/components/chat/chat-header.tsx +299 -314
- package/src/components/{sessions/session-list.tsx → chat/chat-list.tsx} +11 -14
- package/src/components/chat/chat-tool-toggles.tsx +26 -17
- package/src/components/chat/checkpoint-timeline.tsx +4 -4
- package/src/components/chat/message-bubble.tsx +4 -1
- package/src/components/chat/message-list.tsx +5 -3
- package/src/components/chat/session-debug-panel.tsx +1 -1
- package/src/components/chat/tool-request-banner.tsx +3 -3
- package/src/components/chatrooms/agent-hover-card.tsx +3 -3
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +2 -2
- package/src/components/chatrooms/chatroom-view.tsx +347 -205
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +218 -1
- package/src/components/home/home-view.tsx +129 -5
- package/src/components/layout/app-layout.tsx +392 -182
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/plugins/plugin-list.tsx +487 -254
- package/src/components/plugins/plugin-sheet.tsx +236 -13
- package/src/components/projects/project-detail.tsx +183 -0
- package/src/components/settings/gateway-connection-panel.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/command-palette.tsx +111 -25
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +78 -1
- package/src/components/shared/settings/section-orchestrator.tsx +3 -3
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +244 -56
- package/src/components/tasks/approvals-panel.tsx +205 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/usage/metrics-dashboard.tsx +147 -1
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +8 -8
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/chat.ts +1 -1
- package/src/lib/{sessions.ts → chats.ts} +28 -18
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/providers/claude-cli.ts +1 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +205 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +36 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/capability-router.ts +10 -8
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
- package/src/lib/server/chat-execution.ts +285 -165
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +67 -2
- package/src/lib/server/chatroom-helpers.ts +48 -8
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +948 -112
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +188 -9
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/cost.ts +34 -1
- package/src/lib/server/daemon-state.ts +61 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/heartbeat-service.ts +14 -40
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +28 -1103
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +5 -6
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +20 -9
- package/src/lib/server/orchestrator.ts +7 -7
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +207 -0
- package/src/lib/server/plugins.ts +927 -66
- package/src/lib/server/provider-health.ts +38 -6
- package/src/lib/server/queue.ts +13 -28
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -82
- package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
- package/src/lib/server/session-tools/calendar.ts +366 -0
- package/src/lib/server/session-tools/canvas.ts +1 -1
- package/src/lib/server/session-tools/chatroom.ts +4 -2
- package/src/lib/server/session-tools/connector.ts +114 -10
- package/src/lib/server/session-tools/context.ts +21 -5
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +74 -28
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +497 -24
- package/src/lib/server/session-tools/discovery.ts +24 -6
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/edit_file.ts +4 -2
- package/src/lib/server/session-tools/email.ts +320 -0
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +241 -25
- package/src/lib/server/session-tools/git.ts +1 -1
- package/src/lib/server/session-tools/http.ts +1 -1
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +380 -0
- package/src/lib/server/session-tools/index.ts +130 -50
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/memory.ts +172 -3
- package/src/lib/server/session-tools/monitor.ts +151 -8
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.ts +1 -1
- package/src/lib/server/session-tools/openclaw-workspace.ts +1 -1
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +148 -7
- package/src/lib/server/session-tools/plugin-creator.ts +89 -26
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +301 -0
- package/src/lib/server/session-tools/sample-ui.ts +1 -1
- package/src/lib/server/session-tools/sandbox.ts +4 -2
- package/src/lib/server/session-tools/schedule.ts +24 -12
- package/src/lib/server/session-tools/session-info.ts +43 -7
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
- package/src/lib/server/session-tools/shell.ts +5 -2
- package/src/lib/server/session-tools/subagent.ts +194 -28
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +42 -12
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +926 -91
- package/src/lib/server/storage.ts +255 -16
- package/src/lib/server/stream-agent-chat.ts +116 -268
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -10
- package/src/lib/server/tool-aliases.ts +66 -18
- package/src/lib/server/tool-capability-policy.test.ts +9 -9
- package/src/lib/server/tool-capability-policy.ts +38 -27
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/tool-definitions.ts +4 -0
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +10 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +5 -11
- package/src/stores/use-chat-store.ts +38 -9
- package/src/types/index.ts +352 -47
- package/src/app/api/sessions/[id]/main-loop/route.ts +0 -94
- package/src/components/sessions/new-session-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -24
- package/src/lib/server/session-run-manager.test.ts +0 -23
- /package/src/app/api/{sessions → chats}/[id]/clear/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/deploy/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/devserver/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/edit-resend/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/fork/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/mailbox/route.ts +0 -0
- /package/src/app/api/{sessions → chats}/[id]/retry/route.ts +0 -0
- /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
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
{
|
|
302
|
+
{filteredWorkflowApprovals.length > 0 && (
|
|
153
303
|
<div>
|
|
154
|
-
<h2 className="text-[12px] font-700 uppercase tracking-[0.1em] text-amber-400/90 mb-2">
|
|
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
|
-
{
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
|
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(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
[
|
|
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) =>
|
|
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
|
-
|
|
388
|
-
|
|
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
|
×
|
|
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
|
+
×
|
|
545
|
+
</button>
|
|
546
|
+
</span>
|
|
547
|
+
)}
|
|
399
548
|
</div>
|
|
400
|
-
)}
|
|
549
|
+
) : null}
|
|
401
550
|
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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 && (
|