@swarmclawai/swarmclaw 0.7.2 → 0.7.4
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 +116 -50
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- 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/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- 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/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- 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 +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- 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 +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- 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 +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- 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/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -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 +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- 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 +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- 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 +994 -130
- 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 +189 -10
- 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/daemon-state.ts +62 -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/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -43
- 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 +31 -964
- 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 +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- 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 +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- 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 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- 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 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- 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/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -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,16 @@ 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
|
+
const [now, setNow] = useState(() => Date.now())
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const intervalId = window.setInterval(() => setNow(Date.now()), 60_000)
|
|
79
|
+
return () => window.clearInterval(intervalId)
|
|
80
|
+
}, [])
|
|
81
|
+
|
|
62
82
|
const taskApprovals = useMemo(() => {
|
|
63
83
|
return Object.values(tasks)
|
|
64
84
|
.filter((t) => t.pendingApproval)
|
|
@@ -92,6 +112,73 @@ export function ApprovalsPanel() {
|
|
|
92
112
|
}, [execApprovals])
|
|
93
113
|
|
|
94
114
|
const pendingCount = sortedExecApprovals.length + workflowApprovals.length
|
|
115
|
+
const searchTerm = search.trim().toLowerCase()
|
|
116
|
+
|
|
117
|
+
const workflowCategories = useMemo(() => (
|
|
118
|
+
Array.from(new Set(workflowApprovals.map((req) => req.category))).sort()
|
|
119
|
+
), [workflowApprovals])
|
|
120
|
+
|
|
121
|
+
const filteredExecApprovals = useMemo(() => {
|
|
122
|
+
if (scope === 'workflow' || scope === 'task') return []
|
|
123
|
+
return sortedExecApprovals.filter((approval) => {
|
|
124
|
+
if (!searchTerm) return true
|
|
125
|
+
return [
|
|
126
|
+
approval.ask,
|
|
127
|
+
approval.command,
|
|
128
|
+
approval.cwd,
|
|
129
|
+
approval.host,
|
|
130
|
+
approval.security,
|
|
131
|
+
].some((value) => value?.toLowerCase().includes(searchTerm))
|
|
132
|
+
})
|
|
133
|
+
}, [scope, sortedExecApprovals, searchTerm])
|
|
134
|
+
|
|
135
|
+
const filteredWorkflowApprovals = useMemo(() => {
|
|
136
|
+
return workflowApprovals.filter((req) => {
|
|
137
|
+
if (scope === 'execution') return false
|
|
138
|
+
if (scope === 'workflow' && req.category === 'task_tool') return false
|
|
139
|
+
if (scope === 'task' && req.category !== 'task_tool') return false
|
|
140
|
+
if (categoryFilter !== 'all' && req.category !== categoryFilter) return false
|
|
141
|
+
if (!searchTerm) return true
|
|
142
|
+
const agentName = req.agentId ? agents[req.agentId]?.name : 'system'
|
|
143
|
+
const payloadText = JSON.stringify(getApprovalPayload(req))
|
|
144
|
+
return [
|
|
145
|
+
getApprovalTitle(req),
|
|
146
|
+
req.description,
|
|
147
|
+
req.category,
|
|
148
|
+
agentName,
|
|
149
|
+
payloadText,
|
|
150
|
+
].some((value) => value?.toLowerCase().includes(searchTerm))
|
|
151
|
+
})
|
|
152
|
+
}, [agents, categoryFilter, scope, searchTerm, workflowApprovals])
|
|
153
|
+
|
|
154
|
+
const visibleCount = filteredExecApprovals.length + filteredWorkflowApprovals.length
|
|
155
|
+
|
|
156
|
+
const summaryCards = [
|
|
157
|
+
{
|
|
158
|
+
label: 'Execution',
|
|
159
|
+
value: sortedExecApprovals.length,
|
|
160
|
+
tone: 'text-amber-400',
|
|
161
|
+
hint: 'Command approvals from OpenClaw',
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
label: 'Workflow',
|
|
165
|
+
value: sessionApprovals.length,
|
|
166
|
+
tone: 'text-sky-400',
|
|
167
|
+
hint: 'Agent and plugin governance requests',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
label: 'Task Calls',
|
|
171
|
+
value: taskApprovals.length,
|
|
172
|
+
tone: 'text-violet-400',
|
|
173
|
+
hint: 'Tasks waiting on tool approval',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
label: 'Recently Active',
|
|
177
|
+
value: workflowApprovals.filter((req) => now - req.updatedAt < 60 * 60 * 1000).length,
|
|
178
|
+
tone: 'text-emerald-400',
|
|
179
|
+
hint: 'Updated in the last hour',
|
|
180
|
+
},
|
|
181
|
+
]
|
|
95
182
|
|
|
96
183
|
const handleDecision = async (req: ApprovalRequest, approved: boolean) => {
|
|
97
184
|
try {
|
|
@@ -131,29 +218,98 @@ export function ApprovalsPanel() {
|
|
|
131
218
|
<div className="flex items-center justify-between mb-8">
|
|
132
219
|
<div>
|
|
133
220
|
<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
|
|
221
|
+
<p className="text-[13px] text-text-3">Execution, task, and governance requests queued for review</p>
|
|
135
222
|
</div>
|
|
136
223
|
<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
224
|
{pendingCount} Pending
|
|
138
225
|
</div>
|
|
139
226
|
</div>
|
|
140
227
|
|
|
141
|
-
|
|
228
|
+
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3 mb-6">
|
|
229
|
+
{summaryCards.map((card) => (
|
|
230
|
+
<div key={card.label} className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-3.5">
|
|
231
|
+
<div className={`text-[22px] font-display font-700 tracking-[-0.03em] ${card.tone}`}>
|
|
232
|
+
{card.value}
|
|
233
|
+
</div>
|
|
234
|
+
<div className="text-[11px] font-600 text-text-2 mt-0.5">{card.label}</div>
|
|
235
|
+
<p className="text-[10px] text-text-3/50 mt-1 leading-relaxed">{card.hint}</p>
|
|
236
|
+
</div>
|
|
237
|
+
))}
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4 mb-6">
|
|
241
|
+
<div className="flex flex-col lg:flex-row gap-3 lg:items-center lg:justify-between">
|
|
242
|
+
<div className="flex flex-wrap gap-2">
|
|
243
|
+
{([
|
|
244
|
+
['all', `All (${pendingCount})`],
|
|
245
|
+
['execution', `Execution (${sortedExecApprovals.length})`],
|
|
246
|
+
['workflow', `Workflow (${sessionApprovals.length})`],
|
|
247
|
+
['task', `Tasks (${taskApprovals.length})`],
|
|
248
|
+
] as const).map(([value, label]) => (
|
|
249
|
+
<button
|
|
250
|
+
key={value}
|
|
251
|
+
onClick={() => setScope(value)}
|
|
252
|
+
className={`px-3 py-1.5 rounded-[9px] text-[11px] font-600 transition-all cursor-pointer border-none ${
|
|
253
|
+
scope === value
|
|
254
|
+
? 'bg-accent-soft text-accent-bright'
|
|
255
|
+
: 'bg-white/[0.04] text-text-3 hover:bg-white/[0.08] hover:text-text-2'
|
|
256
|
+
}`}
|
|
257
|
+
style={{ fontFamily: 'inherit' }}
|
|
258
|
+
>
|
|
259
|
+
{label}
|
|
260
|
+
</button>
|
|
261
|
+
))}
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
<div className="flex items-center gap-2">
|
|
265
|
+
<div className="text-[11px] text-text-3/60 font-600">
|
|
266
|
+
Showing {visibleCount} of {pendingCount}
|
|
267
|
+
</div>
|
|
268
|
+
{workflowCategories.length > 1 && scope !== 'execution' && (
|
|
269
|
+
<select
|
|
270
|
+
value={categoryFilter}
|
|
271
|
+
onChange={(e) => setCategoryFilter(e.target.value)}
|
|
272
|
+
className="px-3 py-2 rounded-[10px] bg-white/[0.04] border border-white/[0.06] text-[12px] text-text-2 outline-none"
|
|
273
|
+
style={{ fontFamily: 'inherit' }}
|
|
274
|
+
>
|
|
275
|
+
<option value="all">All categories</option>
|
|
276
|
+
{workflowCategories.map((category) => (
|
|
277
|
+
<option key={category} value={category}>
|
|
278
|
+
{CATEGORY_LABELS[category] || category}
|
|
279
|
+
</option>
|
|
280
|
+
))}
|
|
281
|
+
</select>
|
|
282
|
+
)}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
|
|
286
|
+
<div className="mt-3">
|
|
287
|
+
<input
|
|
288
|
+
value={search}
|
|
289
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
290
|
+
placeholder="Search approvals by agent, tool, command, or payload"
|
|
291
|
+
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]"
|
|
292
|
+
style={{ fontFamily: 'inherit' }}
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{filteredExecApprovals.length > 0 && (
|
|
142
298
|
<div className="mb-6">
|
|
143
299
|
<h2 className="text-[12px] font-700 uppercase tracking-[0.1em] text-amber-400/90 mb-2">Execution Approvals</h2>
|
|
144
300
|
<div className="grid grid-cols-1 gap-3">
|
|
145
|
-
{
|
|
301
|
+
{filteredExecApprovals.map((approval) => (
|
|
146
302
|
<ExecApprovalCard key={approval.id} approval={approval} />
|
|
147
303
|
))}
|
|
148
304
|
</div>
|
|
149
305
|
</div>
|
|
150
306
|
)}
|
|
151
307
|
|
|
152
|
-
{
|
|
308
|
+
{filteredWorkflowApprovals.length > 0 && (
|
|
153
309
|
<div>
|
|
154
|
-
<h2 className="text-[12px] font-700 uppercase tracking-[0.1em] text-amber-400/90 mb-2">
|
|
310
|
+
<h2 className="text-[12px] font-700 uppercase tracking-[0.1em] text-amber-400/90 mb-2">Workflow Approvals</h2>
|
|
155
311
|
<div className="grid grid-cols-1 gap-4">
|
|
156
|
-
{
|
|
312
|
+
{filteredWorkflowApprovals.map((req) => {
|
|
157
313
|
const agent = req.agentId ? agents[req.agentId] : null
|
|
158
314
|
const icon = CATEGORY_ICONS[req.category] || '⚠️'
|
|
159
315
|
const categoryLabel = CATEGORY_LABELS[req.category] || req.category
|
|
@@ -173,10 +329,23 @@ export function ApprovalsPanel() {
|
|
|
173
329
|
<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
330
|
{categoryLabel}
|
|
175
331
|
</span>
|
|
332
|
+
{req.taskId && (
|
|
333
|
+
<span className="px-1.5 py-0.5 rounded-[4px] bg-violet-500/10 text-[9px] font-700 text-violet-300 uppercase tracking-wider">
|
|
334
|
+
Task
|
|
335
|
+
</span>
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
<div className="flex flex-wrap items-center gap-x-2 gap-y-1 mt-1 text-[11px] text-text-3">
|
|
339
|
+
<span>{agent?.name || 'System'}</span>
|
|
340
|
+
<span className="text-text-3/35">•</span>
|
|
341
|
+
<span>{relativeTime(req.updatedAt)}</span>
|
|
342
|
+
{req.description && (
|
|
343
|
+
<>
|
|
344
|
+
<span className="text-text-3/35">•</span>
|
|
345
|
+
<span className="truncate max-w-[280px]">{req.description}</span>
|
|
346
|
+
</>
|
|
347
|
+
)}
|
|
176
348
|
</div>
|
|
177
|
-
<p className="text-[11px] text-text-3">
|
|
178
|
-
{agent?.name || 'System'}
|
|
179
|
-
</p>
|
|
180
349
|
</div>
|
|
181
350
|
</div>
|
|
182
351
|
<span className="text-[10px] text-text-3/50 font-mono">
|
|
@@ -185,16 +354,33 @@ export function ApprovalsPanel() {
|
|
|
185
354
|
</div>
|
|
186
355
|
|
|
187
356
|
<div className="p-5">
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
357
|
+
<div className="flex flex-wrap gap-2 mb-4">
|
|
358
|
+
{req.sessionId && (
|
|
359
|
+
<span className="px-2 py-1 rounded-[7px] bg-white/[0.04] text-[10px] font-600 text-text-3">
|
|
360
|
+
Session {req.sessionId.slice(0, 8)}
|
|
361
|
+
</span>
|
|
362
|
+
)}
|
|
363
|
+
{req.taskId && (
|
|
364
|
+
<span className="px-2 py-1 rounded-[7px] bg-violet-500/10 text-[10px] font-600 text-violet-300">
|
|
365
|
+
Task {req.taskId.slice(0, 8)}
|
|
366
|
+
</span>
|
|
367
|
+
)}
|
|
196
368
|
</div>
|
|
197
369
|
|
|
370
|
+
<details className="mb-5 rounded-[10px] border border-white/[0.04] bg-black/20 overflow-hidden group">
|
|
371
|
+
<summary className="list-none cursor-pointer px-4 py-3 flex items-center justify-between text-[12px] font-600 text-text-2">
|
|
372
|
+
<span>Payload details</span>
|
|
373
|
+
<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">
|
|
374
|
+
<polyline points="6 9 12 15 18 9" />
|
|
375
|
+
</svg>
|
|
376
|
+
</summary>
|
|
377
|
+
<div className="px-4 pb-4 overflow-x-auto max-h-[260px] overflow-y-auto">
|
|
378
|
+
<pre className="text-[12px] font-mono text-text-2/80 whitespace-pre-wrap break-all leading-relaxed">
|
|
379
|
+
{payloadText === '{}' ? 'No structured payload provided.' : payloadText}
|
|
380
|
+
</pre>
|
|
381
|
+
</div>
|
|
382
|
+
</details>
|
|
383
|
+
|
|
198
384
|
<div className="flex items-center justify-end gap-3 pt-4 border-t border-white/[0.04]">
|
|
199
385
|
<button
|
|
200
386
|
onClick={() => handleDecision(req, false)}
|
|
@@ -218,6 +404,13 @@ export function ApprovalsPanel() {
|
|
|
218
404
|
</div>
|
|
219
405
|
</div>
|
|
220
406
|
)}
|
|
407
|
+
|
|
408
|
+
{visibleCount === 0 && pendingCount > 0 && (
|
|
409
|
+
<div className="rounded-[16px] border border-dashed border-white/[0.08] px-6 py-10 text-center">
|
|
410
|
+
<p className="text-[13px] font-600 text-text-2 mb-1">No approvals match the current filters</p>
|
|
411
|
+
<p className="text-[12px] text-text-3/60">Try clearing the search or switching the queue scope.</p>
|
|
412
|
+
</div>
|
|
413
|
+
)}
|
|
221
414
|
</div>
|
|
222
415
|
</div>
|
|
223
416
|
)
|
|
@@ -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 && (
|