@swarmclawai/swarmclaw 0.5.2 → 0.6.0
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 +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useCallback, useState } from 'react'
|
|
3
|
+
import { useEffect, useCallback, useState, useRef, useMemo } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { useWs } from '@/hooks/use-ws'
|
|
6
|
-
import { updateTask } from '@/lib/tasks'
|
|
6
|
+
import { updateTask, bulkUpdateTasks } from '@/lib/tasks'
|
|
7
7
|
import { TaskColumn } from './task-column'
|
|
8
|
+
import { Skeleton } from '@/components/shared/skeleton'
|
|
9
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
10
|
import type { BoardTaskStatus } from '@/types'
|
|
11
|
+
import { toast } from 'sonner'
|
|
9
12
|
|
|
10
13
|
const ACTIVE_COLUMNS: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed']
|
|
11
14
|
|
|
@@ -16,8 +19,98 @@ export function TaskBoard() {
|
|
|
16
19
|
const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
|
|
17
20
|
const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
|
|
18
21
|
const agents = useAppStore((s) => s.agents)
|
|
22
|
+
const projects = useAppStore((s) => s.projects)
|
|
23
|
+
const loadProjects = useAppStore((s) => s.loadProjects)
|
|
24
|
+
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
25
|
+
const setActiveProjectFilter = useAppStore((s) => s.setActiveProjectFilter)
|
|
19
26
|
const showArchived = useAppStore((s) => s.showArchivedTasks)
|
|
20
27
|
const setShowArchived = useAppStore((s) => s.setShowArchivedTasks)
|
|
28
|
+
|
|
29
|
+
// Bulk selection
|
|
30
|
+
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
|
31
|
+
const selectionMode = selectedIds.size > 0
|
|
32
|
+
|
|
33
|
+
const toggleSelect = useCallback((id: string) => {
|
|
34
|
+
setSelectedIds((prev) => {
|
|
35
|
+
const next = new Set(prev)
|
|
36
|
+
if (next.has(id)) next.delete(id)
|
|
37
|
+
else next.add(id)
|
|
38
|
+
return next
|
|
39
|
+
})
|
|
40
|
+
}, [])
|
|
41
|
+
|
|
42
|
+
const clearSelection = useCallback(() => setSelectedIds(new Set()), [])
|
|
43
|
+
|
|
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
|
+
// Bulk action handlers
|
|
56
|
+
const [bulkActing, setBulkActing] = useState(false)
|
|
57
|
+
const handleBulkStatus = useCallback(async (status: BoardTaskStatus) => {
|
|
58
|
+
if (selectedIds.size === 0) return
|
|
59
|
+
setBulkActing(true)
|
|
60
|
+
try {
|
|
61
|
+
await bulkUpdateTasks([...selectedIds], { status })
|
|
62
|
+
await loadTasks()
|
|
63
|
+
toast.success(`Moved ${selectedIds.size} task(s) to ${status}`)
|
|
64
|
+
clearSelection()
|
|
65
|
+
} catch {
|
|
66
|
+
toast.error('Bulk update failed')
|
|
67
|
+
} finally {
|
|
68
|
+
setBulkActing(false)
|
|
69
|
+
}
|
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
|
+
}, [selectedIds])
|
|
72
|
+
|
|
73
|
+
const handleBulkAgent = useCallback(async (agentId: string) => {
|
|
74
|
+
if (selectedIds.size === 0) return
|
|
75
|
+
setBulkActing(true)
|
|
76
|
+
try {
|
|
77
|
+
await bulkUpdateTasks([...selectedIds], { agentId })
|
|
78
|
+
await loadTasks()
|
|
79
|
+
const name = agents[agentId]?.name || 'agent'
|
|
80
|
+
toast.success(`Assigned ${selectedIds.size} task(s) to ${name}`)
|
|
81
|
+
clearSelection()
|
|
82
|
+
} catch {
|
|
83
|
+
toast.error('Bulk assign failed')
|
|
84
|
+
} finally {
|
|
85
|
+
setBulkActing(false)
|
|
86
|
+
}
|
|
87
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
88
|
+
}, [selectedIds, agents])
|
|
89
|
+
|
|
90
|
+
const handleBulkProject = useCallback(async (projectId: string | null) => {
|
|
91
|
+
if (selectedIds.size === 0) return
|
|
92
|
+
setBulkActing(true)
|
|
93
|
+
try {
|
|
94
|
+
await bulkUpdateTasks([...selectedIds], { projectId })
|
|
95
|
+
await loadTasks()
|
|
96
|
+
toast.success(projectId ? `Assigned ${selectedIds.size} task(s) to project` : `Cleared project from ${selectedIds.size} task(s)`)
|
|
97
|
+
clearSelection()
|
|
98
|
+
} catch {
|
|
99
|
+
toast.error('Bulk assign failed')
|
|
100
|
+
} finally {
|
|
101
|
+
setBulkActing(false)
|
|
102
|
+
}
|
|
103
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
104
|
+
}, [selectedIds])
|
|
105
|
+
|
|
106
|
+
// Bulk action bar dropdowns
|
|
107
|
+
const [bulkAgentOpen, setBulkAgentOpen] = useState(false)
|
|
108
|
+
const [bulkProjectOpen, setBulkProjectOpen] = useState(false)
|
|
109
|
+
const [bulkStatusOpen, setBulkStatusOpen] = useState(false)
|
|
110
|
+
const bulkAgentRef = useRef<HTMLDivElement>(null)
|
|
111
|
+
const bulkProjectRef = useRef<HTMLDivElement>(null)
|
|
112
|
+
const bulkStatusRef = useRef<HTMLDivElement>(null)
|
|
113
|
+
|
|
21
114
|
// URL-based filter state
|
|
22
115
|
const [filterAgentId, setFilterAgentId] = useState<string>(() => {
|
|
23
116
|
if (typeof window === 'undefined') return ''
|
|
@@ -28,18 +121,30 @@ export function TaskBoard() {
|
|
|
28
121
|
return new URLSearchParams(window.location.search).get('tag') || ''
|
|
29
122
|
})
|
|
30
123
|
|
|
124
|
+
// Seed activeProjectFilter from URL on mount
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (typeof window === 'undefined') return
|
|
127
|
+
const urlProject = new URLSearchParams(window.location.search).get('project')
|
|
128
|
+
if (urlProject && !activeProjectFilter) {
|
|
129
|
+
setActiveProjectFilter(urlProject)
|
|
130
|
+
}
|
|
131
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
132
|
+
}, [])
|
|
133
|
+
|
|
31
134
|
// Sync filters to URL
|
|
32
135
|
useEffect(() => {
|
|
33
136
|
if (typeof window === 'undefined') return
|
|
34
137
|
const params = new URLSearchParams()
|
|
35
138
|
if (filterAgentId) params.set('agent', filterAgentId)
|
|
36
139
|
if (filterTag) params.set('tag', filterTag)
|
|
140
|
+
if (activeProjectFilter) params.set('project', activeProjectFilter)
|
|
37
141
|
const qs = params.toString()
|
|
38
142
|
const newUrl = `${window.location.pathname}${qs ? `?${qs}` : ''}`
|
|
39
143
|
window.history.replaceState(null, '', newUrl)
|
|
40
|
-
}, [filterAgentId, filterTag])
|
|
144
|
+
}, [filterAgentId, filterTag, activeProjectFilter])
|
|
41
145
|
|
|
42
|
-
|
|
146
|
+
const [loaded, setLoaded] = useState(Object.keys(tasks).length > 0)
|
|
147
|
+
useEffect(() => { Promise.all([loadTasks(), loadAgents(), loadProjects()]).then(() => setLoaded(true)) }, [])
|
|
43
148
|
useWs('tasks', loadTasks, 5000)
|
|
44
149
|
|
|
45
150
|
// Collect all unique tags across tasks
|
|
@@ -47,42 +152,200 @@ export function TaskBoard() {
|
|
|
47
152
|
|
|
48
153
|
const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
|
|
49
154
|
|
|
50
|
-
const tasksByStatus = (status: BoardTaskStatus) =>
|
|
155
|
+
const tasksByStatus = useCallback((status: BoardTaskStatus) =>
|
|
51
156
|
Object.values(tasks)
|
|
52
157
|
.filter((t) => t.status === status
|
|
53
158
|
&& (!filterAgentId || t.agentId === filterAgentId)
|
|
54
|
-
&& (!filterTag || (t.tags && t.tags.includes(filterTag)))
|
|
55
|
-
|
|
159
|
+
&& (!filterTag || (t.tags && t.tags.includes(filterTag)))
|
|
160
|
+
&& (!activeProjectFilter || t.projectId === activeProjectFilter))
|
|
161
|
+
.sort((a, b) => b.updatedAt - a.updatedAt),
|
|
162
|
+
[tasks, filterAgentId, filterTag, activeProjectFilter])
|
|
56
163
|
|
|
57
164
|
const handleDrop = useCallback(async (taskId: string, newStatus: BoardTaskStatus) => {
|
|
58
165
|
const task = tasks[taskId]
|
|
59
166
|
if (!task || task.status === newStatus) return
|
|
60
167
|
await updateTask(taskId, { status: newStatus })
|
|
61
168
|
await loadTasks()
|
|
62
|
-
|
|
169
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
170
|
+
}, [tasks])
|
|
63
171
|
|
|
64
172
|
const archivedCount = Object.values(tasks).filter((t) => t.status === 'archived').length
|
|
65
173
|
|
|
174
|
+
// Task counts per project (non-archived)
|
|
175
|
+
const projectTaskCounts: Record<string, number> = {}
|
|
176
|
+
for (const t of Object.values(tasks)) {
|
|
177
|
+
if (t.projectId && t.status !== 'archived') {
|
|
178
|
+
projectTaskCounts[t.projectId] = (projectTaskCounts[t.projectId] || 0) + 1
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Summary stats
|
|
183
|
+
const stats = useMemo(() => {
|
|
184
|
+
const all = Object.values(tasks).filter((t) => t.status !== 'archived')
|
|
185
|
+
return {
|
|
186
|
+
total: all.length,
|
|
187
|
+
running: all.filter((t) => t.status === 'running').length,
|
|
188
|
+
completed: all.filter((t) => t.status === 'completed').length,
|
|
189
|
+
failed: all.filter((t) => t.status === 'failed').length,
|
|
190
|
+
overdue: all.filter((t) => t.dueAt && t.dueAt < Date.now() && t.status !== 'completed').length,
|
|
191
|
+
}
|
|
192
|
+
}, [tasks])
|
|
193
|
+
|
|
194
|
+
// Custom dropdown state
|
|
195
|
+
const [projectDropdownOpen, setProjectDropdownOpen] = useState(false)
|
|
196
|
+
const projectDropdownRef = useRef<HTMLDivElement>(null)
|
|
197
|
+
const [agentDropdownOpen, setAgentDropdownOpen] = useState(false)
|
|
198
|
+
const agentDropdownRef = useRef<HTMLDivElement>(null)
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (!projectDropdownOpen && !agentDropdownOpen && !bulkAgentOpen && !bulkProjectOpen && !bulkStatusOpen) return
|
|
201
|
+
const onClickOutside = (e: MouseEvent) => {
|
|
202
|
+
if (projectDropdownOpen && projectDropdownRef.current && !projectDropdownRef.current.contains(e.target as Node)) {
|
|
203
|
+
setProjectDropdownOpen(false)
|
|
204
|
+
}
|
|
205
|
+
if (agentDropdownOpen && agentDropdownRef.current && !agentDropdownRef.current.contains(e.target as Node)) {
|
|
206
|
+
setAgentDropdownOpen(false)
|
|
207
|
+
}
|
|
208
|
+
if (bulkAgentOpen && bulkAgentRef.current && !bulkAgentRef.current.contains(e.target as Node)) {
|
|
209
|
+
setBulkAgentOpen(false)
|
|
210
|
+
}
|
|
211
|
+
if (bulkProjectOpen && bulkProjectRef.current && !bulkProjectRef.current.contains(e.target as Node)) {
|
|
212
|
+
setBulkProjectOpen(false)
|
|
213
|
+
}
|
|
214
|
+
if (bulkStatusOpen && bulkStatusRef.current && !bulkStatusRef.current.contains(e.target as Node)) {
|
|
215
|
+
setBulkStatusOpen(false)
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
document.addEventListener('mousedown', onClickOutside)
|
|
219
|
+
return () => document.removeEventListener('mousedown', onClickOutside)
|
|
220
|
+
}, [projectDropdownOpen, agentDropdownOpen, bulkAgentOpen, bulkProjectOpen, bulkStatusOpen])
|
|
221
|
+
|
|
222
|
+
// Escape key to clear selection
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (!selectionMode) return
|
|
225
|
+
const handler = (e: KeyboardEvent) => {
|
|
226
|
+
if (e.key === 'Escape') clearSelection()
|
|
227
|
+
}
|
|
228
|
+
window.addEventListener('keydown', handler)
|
|
229
|
+
return () => window.removeEventListener('keydown', handler)
|
|
230
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
231
|
+
}, [selectionMode])
|
|
232
|
+
|
|
66
233
|
return (
|
|
67
234
|
<div className="flex-1 flex flex-col h-full overflow-hidden">
|
|
68
235
|
<div className="flex items-center justify-between px-8 pt-6 pb-4 shrink-0">
|
|
69
236
|
<div>
|
|
70
237
|
<h1 className="font-display text-[28px] font-800 tracking-[-0.03em]">Task Board</h1>
|
|
71
|
-
<
|
|
238
|
+
<div className="flex items-center gap-3 mt-1">
|
|
239
|
+
<p className="text-[13px] text-text-3">
|
|
240
|
+
{stats.total} task{stats.total !== 1 ? 's' : ''}
|
|
241
|
+
</p>
|
|
242
|
+
{stats.running > 0 && (
|
|
243
|
+
<span className="inline-flex items-center gap-1 text-[11px] font-600 text-blue-400">
|
|
244
|
+
<span className="w-1.5 h-1.5 rounded-full bg-blue-400 animate-pulse" />
|
|
245
|
+
{stats.running} running
|
|
246
|
+
</span>
|
|
247
|
+
)}
|
|
248
|
+
{stats.overdue > 0 && (
|
|
249
|
+
<span className="text-[11px] font-600 text-red-400">
|
|
250
|
+
{stats.overdue} overdue
|
|
251
|
+
</span>
|
|
252
|
+
)}
|
|
253
|
+
</div>
|
|
72
254
|
</div>
|
|
73
255
|
<div className="flex items-center gap-3">
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
256
|
+
<div className="relative" ref={agentDropdownRef}>
|
|
257
|
+
<button
|
|
258
|
+
onClick={() => setAgentDropdownOpen(!agentDropdownOpen)}
|
|
259
|
+
className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
260
|
+
${filterAgentId
|
|
261
|
+
? 'bg-white/[0.06] border-white/[0.1] text-text-2'
|
|
262
|
+
: 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
|
|
263
|
+
style={{ fontFamily: 'inherit', minWidth: 130 }}
|
|
264
|
+
>
|
|
265
|
+
{filterAgentId && agents[filterAgentId] ? (
|
|
266
|
+
<>
|
|
267
|
+
<AgentAvatar seed={agents[filterAgentId].avatarSeed || null} name={agents[filterAgentId].name} size={18} />
|
|
268
|
+
{agents[filterAgentId].name}
|
|
269
|
+
</>
|
|
270
|
+
) : 'All Agents'}
|
|
271
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" className="ml-auto opacity-50">
|
|
272
|
+
<path d="M2.5 4L5 6.5L7.5 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
273
|
+
</svg>
|
|
274
|
+
</button>
|
|
275
|
+
{agentDropdownOpen && (
|
|
276
|
+
<div className="absolute top-full right-0 mt-1 min-w-[200px] py-1 rounded-[12px] border border-white/[0.08] bg-surface-2 shadow-lg z-50">
|
|
277
|
+
<button
|
|
278
|
+
onClick={() => { setFilterAgentId(''); setAgentDropdownOpen(false) }}
|
|
279
|
+
className={`w-full flex items-center gap-2.5 px-3 py-2 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
|
|
280
|
+
${!filterAgentId ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
|
|
281
|
+
style={{ fontFamily: 'inherit' }}
|
|
282
|
+
>
|
|
283
|
+
All Agents
|
|
284
|
+
</button>
|
|
285
|
+
{Object.values(agents).sort((a, b) => a.name.localeCompare(b.name)).map((a) => (
|
|
286
|
+
<button
|
|
287
|
+
key={a.id}
|
|
288
|
+
onClick={() => { setFilterAgentId(a.id); setAgentDropdownOpen(false) }}
|
|
289
|
+
className={`w-full flex items-center gap-2.5 px-3 py-2 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
|
|
290
|
+
${filterAgentId === a.id ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
|
|
291
|
+
style={{ fontFamily: 'inherit' }}
|
|
292
|
+
>
|
|
293
|
+
<AgentAvatar seed={a.avatarSeed || null} name={a.name} size={20} />
|
|
294
|
+
{a.name}
|
|
295
|
+
</button>
|
|
296
|
+
))}
|
|
297
|
+
</div>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
{Object.keys(projects).length > 0 && (
|
|
301
|
+
<div className="relative" ref={projectDropdownRef}>
|
|
302
|
+
<button
|
|
303
|
+
onClick={() => setProjectDropdownOpen(!projectDropdownOpen)}
|
|
304
|
+
className={`flex items-center gap-2 px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
305
|
+
${activeProjectFilter
|
|
306
|
+
? 'bg-white/[0.06] border-white/[0.1] text-text-2'
|
|
307
|
+
: 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
|
|
308
|
+
style={{ fontFamily: 'inherit', minWidth: 130 }}
|
|
309
|
+
>
|
|
310
|
+
{activeProjectFilter && projects[activeProjectFilter] ? (
|
|
311
|
+
<>
|
|
312
|
+
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: projects[activeProjectFilter].color || '#6366F1' }} />
|
|
313
|
+
{projects[activeProjectFilter].name}
|
|
314
|
+
</>
|
|
315
|
+
) : 'All Projects'}
|
|
316
|
+
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" className="ml-auto opacity-50">
|
|
317
|
+
<path d="M2.5 4L5 6.5L7.5 4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
318
|
+
</svg>
|
|
319
|
+
</button>
|
|
320
|
+
{projectDropdownOpen && (
|
|
321
|
+
<div className="absolute top-full right-0 mt-1 min-w-[180px] py-1 rounded-[12px] border border-white/[0.08] bg-surface-2 shadow-lg z-50">
|
|
322
|
+
<button
|
|
323
|
+
onClick={() => { setActiveProjectFilter(null); setProjectDropdownOpen(false) }}
|
|
324
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
|
|
325
|
+
${!activeProjectFilter ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
|
|
326
|
+
style={{ fontFamily: 'inherit' }}
|
|
327
|
+
>
|
|
328
|
+
All Projects
|
|
329
|
+
</button>
|
|
330
|
+
{Object.values(projects).map((p) => (
|
|
331
|
+
<button
|
|
332
|
+
key={p.id}
|
|
333
|
+
onClick={() => { setActiveProjectFilter(p.id); setProjectDropdownOpen(false) }}
|
|
334
|
+
className={`w-full flex items-center gap-2 px-3 py-2 text-[13px] font-600 cursor-pointer border-none text-left transition-colors
|
|
335
|
+
${activeProjectFilter === p.id ? 'bg-white/[0.06] text-text' : 'bg-transparent text-text-3 hover:bg-white/[0.04]'}`}
|
|
336
|
+
style={{ fontFamily: 'inherit' }}
|
|
337
|
+
>
|
|
338
|
+
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: p.color || '#6366F1' }} />
|
|
339
|
+
{p.name}
|
|
340
|
+
{(projectTaskCounts[p.id] ?? 0) > 0 && (
|
|
341
|
+
<span className="ml-auto text-[11px] text-text-3/60">{projectTaskCounts[p.id]}</span>
|
|
342
|
+
)}
|
|
343
|
+
</button>
|
|
344
|
+
))}
|
|
345
|
+
</div>
|
|
346
|
+
)}
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
86
349
|
{allTags.length > 0 && (
|
|
87
350
|
<select
|
|
88
351
|
value={filterTag}
|
|
@@ -105,14 +368,14 @@ export function TaskBoard() {
|
|
|
105
368
|
: 'bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03]'}`}
|
|
106
369
|
style={{ fontFamily: 'inherit' }}
|
|
107
370
|
>
|
|
108
|
-
{showArchived ? 'Hide' : 'Show'} Archived{!showArchived && archivedCount > 0 ?
|
|
371
|
+
{showArchived ? 'Hide' : 'Show'} Archived{!showArchived && archivedCount > 0 ? ` (${archivedCount})` : ''}
|
|
109
372
|
</button>
|
|
110
373
|
<button
|
|
111
374
|
onClick={() => {
|
|
112
375
|
setEditingTaskId(null)
|
|
113
376
|
setTaskSheetOpen(true)
|
|
114
377
|
}}
|
|
115
|
-
className="px-5 py-2.5 rounded-[12px] border-none bg-
|
|
378
|
+
className="px-5 py-2.5 rounded-[12px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer
|
|
116
379
|
hover:brightness-110 active:scale-[0.97] transition-all shadow-[0_2px_12px_rgba(99,102,241,0.2)]"
|
|
117
380
|
style={{ fontFamily: 'inherit' }}
|
|
118
381
|
>
|
|
@@ -121,11 +384,170 @@ export function TaskBoard() {
|
|
|
121
384
|
</div>
|
|
122
385
|
</div>
|
|
123
386
|
|
|
387
|
+
{activeProjectFilter && projects[activeProjectFilter] && (
|
|
388
|
+
<div className="flex items-center gap-2 px-8 pb-3">
|
|
389
|
+
<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
|
+
<span className="w-2 h-2 rounded-full" style={{ backgroundColor: projects[activeProjectFilter].color || '#6366F1' }} />
|
|
391
|
+
{projects[activeProjectFilter].name}
|
|
392
|
+
<button
|
|
393
|
+
onClick={() => setActiveProjectFilter(null)}
|
|
394
|
+
className="ml-1 text-text-3 hover:text-text cursor-pointer border-none bg-transparent p-0 text-[14px] leading-none"
|
|
395
|
+
>
|
|
396
|
+
×
|
|
397
|
+
</button>
|
|
398
|
+
</span>
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
|
|
124
402
|
<div className="flex-1 flex gap-5 px-8 pb-6 overflow-x-auto overflow-y-hidden">
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
403
|
+
{!loaded ? (
|
|
404
|
+
ACTIVE_COLUMNS.map((status) => (
|
|
405
|
+
<div key={status} className="flex flex-col gap-3 min-w-[260px] flex-1">
|
|
406
|
+
<Skeleton className="rounded-[10px]" width="100%" height={32} />
|
|
407
|
+
{Array.from({ length: 2 }).map((_, i) => (
|
|
408
|
+
<Skeleton key={i} className="rounded-[12px]" width="100%" height={80} />
|
|
409
|
+
))}
|
|
410
|
+
</div>
|
|
411
|
+
))
|
|
412
|
+
) : (
|
|
413
|
+
columns.map((status) => (
|
|
414
|
+
<TaskColumn
|
|
415
|
+
key={status}
|
|
416
|
+
status={status}
|
|
417
|
+
tasks={tasksByStatus(status)}
|
|
418
|
+
onDrop={handleDrop}
|
|
419
|
+
selectionMode={selectionMode}
|
|
420
|
+
selectedIds={selectedIds}
|
|
421
|
+
onToggleSelect={toggleSelect}
|
|
422
|
+
onSelectAll={() => selectAllInColumn(status)}
|
|
423
|
+
/>
|
|
424
|
+
))
|
|
425
|
+
)}
|
|
128
426
|
</div>
|
|
427
|
+
|
|
428
|
+
{/* Bulk action bar */}
|
|
429
|
+
{selectionMode && (
|
|
430
|
+
<div className="absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center gap-2 px-4 py-3 rounded-[16px] bg-surface-2/95 backdrop-blur-xl border border-white/[0.1] shadow-[0_8px_40px_rgba(0,0,0,0.5)] z-50">
|
|
431
|
+
<span className="text-[13px] font-600 text-text mr-2">
|
|
432
|
+
{selectedIds.size} selected
|
|
433
|
+
</span>
|
|
434
|
+
<div className="w-px h-5 bg-white/[0.08]" />
|
|
435
|
+
|
|
436
|
+
{/* Move to status */}
|
|
437
|
+
<div className="relative" ref={bulkStatusRef}>
|
|
438
|
+
<button
|
|
439
|
+
onClick={() => { setBulkStatusOpen(!bulkStatusOpen); setBulkAgentOpen(false); setBulkProjectOpen(false) }}
|
|
440
|
+
disabled={bulkActing}
|
|
441
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[12px] font-600 text-text-2 bg-white/[0.06] border-none cursor-pointer hover:bg-white/[0.1] transition-colors disabled:opacity-50"
|
|
442
|
+
style={{ fontFamily: 'inherit' }}
|
|
443
|
+
>
|
|
444
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M5 12h14M12 5l7 7-7 7" /></svg>
|
|
445
|
+
Move
|
|
446
|
+
</button>
|
|
447
|
+
{bulkStatusOpen && (
|
|
448
|
+
<div className="absolute bottom-full left-0 mb-1 min-w-[140px] py-1 rounded-[10px] border border-white/[0.08] bg-surface-2 shadow-lg">
|
|
449
|
+
{ACTIVE_COLUMNS.map((s) => (
|
|
450
|
+
<button
|
|
451
|
+
key={s}
|
|
452
|
+
onClick={() => { handleBulkStatus(s); setBulkStatusOpen(false) }}
|
|
453
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-600 cursor-pointer border-none text-left bg-transparent text-text-3 hover:bg-white/[0.06] hover:text-text transition-colors"
|
|
454
|
+
style={{ fontFamily: 'inherit' }}
|
|
455
|
+
>
|
|
456
|
+
{s.charAt(0).toUpperCase() + s.slice(1)}
|
|
457
|
+
</button>
|
|
458
|
+
))}
|
|
459
|
+
</div>
|
|
460
|
+
)}
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
{/* Assign agent */}
|
|
464
|
+
<div className="relative" ref={bulkAgentRef}>
|
|
465
|
+
<button
|
|
466
|
+
onClick={() => { setBulkAgentOpen(!bulkAgentOpen); setBulkStatusOpen(false); setBulkProjectOpen(false) }}
|
|
467
|
+
disabled={bulkActing}
|
|
468
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[12px] font-600 text-text-2 bg-white/[0.06] border-none cursor-pointer hover:bg-white/[0.1] transition-colors disabled:opacity-50"
|
|
469
|
+
style={{ fontFamily: 'inherit' }}
|
|
470
|
+
>
|
|
471
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" /><circle cx="9" cy="7" r="4" /></svg>
|
|
472
|
+
Agent
|
|
473
|
+
</button>
|
|
474
|
+
{bulkAgentOpen && (
|
|
475
|
+
<div className="absolute bottom-full left-0 mb-1 min-w-[180px] max-h-[200px] overflow-y-auto py-1 rounded-[10px] border border-white/[0.08] bg-surface-2 shadow-lg">
|
|
476
|
+
{Object.values(agents).sort((a, b) => a.name.localeCompare(b.name)).map((a) => (
|
|
477
|
+
<button
|
|
478
|
+
key={a.id}
|
|
479
|
+
onClick={() => { handleBulkAgent(a.id); setBulkAgentOpen(false) }}
|
|
480
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-600 cursor-pointer border-none text-left bg-transparent text-text-3 hover:bg-white/[0.06] hover:text-text transition-colors"
|
|
481
|
+
style={{ fontFamily: 'inherit' }}
|
|
482
|
+
>
|
|
483
|
+
<AgentAvatar seed={a.avatarSeed || null} name={a.name} size={16} />
|
|
484
|
+
{a.name}
|
|
485
|
+
</button>
|
|
486
|
+
))}
|
|
487
|
+
</div>
|
|
488
|
+
)}
|
|
489
|
+
</div>
|
|
490
|
+
|
|
491
|
+
{/* Assign project */}
|
|
492
|
+
{Object.keys(projects).length > 0 && (
|
|
493
|
+
<div className="relative" ref={bulkProjectRef}>
|
|
494
|
+
<button
|
|
495
|
+
onClick={() => { setBulkProjectOpen(!bulkProjectOpen); setBulkStatusOpen(false); setBulkAgentOpen(false) }}
|
|
496
|
+
disabled={bulkActing}
|
|
497
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[12px] font-600 text-text-2 bg-white/[0.06] border-none cursor-pointer hover:bg-white/[0.1] transition-colors disabled:opacity-50"
|
|
498
|
+
style={{ fontFamily: 'inherit' }}
|
|
499
|
+
>
|
|
500
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7-7H4a2 2 0 0 0-2 2v17Z" /><path d="M14 2v7h7" /></svg>
|
|
501
|
+
Project
|
|
502
|
+
</button>
|
|
503
|
+
{bulkProjectOpen && (
|
|
504
|
+
<div className="absolute bottom-full left-0 mb-1 min-w-[160px] max-h-[200px] overflow-y-auto py-1 rounded-[10px] border border-white/[0.08] bg-surface-2 shadow-lg">
|
|
505
|
+
<button
|
|
506
|
+
onClick={() => { handleBulkProject(null); setBulkProjectOpen(false) }}
|
|
507
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-600 cursor-pointer border-none text-left bg-transparent text-text-3 hover:bg-white/[0.06] hover:text-text transition-colors"
|
|
508
|
+
style={{ fontFamily: 'inherit' }}
|
|
509
|
+
>
|
|
510
|
+
No project
|
|
511
|
+
</button>
|
|
512
|
+
{Object.values(projects).map((p) => (
|
|
513
|
+
<button
|
|
514
|
+
key={p.id}
|
|
515
|
+
onClick={() => { handleBulkProject(p.id); setBulkProjectOpen(false) }}
|
|
516
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-600 cursor-pointer border-none text-left bg-transparent text-text-3 hover:bg-white/[0.06] hover:text-text transition-colors"
|
|
517
|
+
style={{ fontFamily: 'inherit' }}
|
|
518
|
+
>
|
|
519
|
+
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: p.color || '#6366F1' }} />
|
|
520
|
+
{p.name}
|
|
521
|
+
</button>
|
|
522
|
+
))}
|
|
523
|
+
</div>
|
|
524
|
+
)}
|
|
525
|
+
</div>
|
|
526
|
+
)}
|
|
527
|
+
|
|
528
|
+
<div className="w-px h-5 bg-white/[0.08]" />
|
|
529
|
+
|
|
530
|
+
{/* Archive selected */}
|
|
531
|
+
<button
|
|
532
|
+
onClick={() => handleBulkStatus('archived')}
|
|
533
|
+
disabled={bulkActing}
|
|
534
|
+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[12px] font-600 text-amber-400 bg-amber-500/10 border-none cursor-pointer hover:bg-amber-500/20 transition-colors disabled:opacity-50"
|
|
535
|
+
style={{ fontFamily: 'inherit' }}
|
|
536
|
+
>
|
|
537
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M21 8v13H3V8" /><path d="M1 3h22v5H1z" /><path d="M10 12h4" /></svg>
|
|
538
|
+
Archive
|
|
539
|
+
</button>
|
|
540
|
+
|
|
541
|
+
{/* Clear selection */}
|
|
542
|
+
<button
|
|
543
|
+
onClick={clearSelection}
|
|
544
|
+
className="p-1.5 rounded-[8px] text-text-3 hover:text-text hover:bg-white/[0.06] border-none bg-transparent cursor-pointer transition-colors"
|
|
545
|
+
title="Clear selection (Esc)"
|
|
546
|
+
>
|
|
547
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" /></svg>
|
|
548
|
+
</button>
|
|
549
|
+
</div>
|
|
550
|
+
)}
|
|
129
551
|
</div>
|
|
130
552
|
)
|
|
131
553
|
}
|
|
@@ -14,8 +14,16 @@ function timeAgo(ts: number) {
|
|
|
14
14
|
return `${Math.floor(diff / 86400_000)}d ago`
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
interface TaskCardProps {
|
|
18
|
+
task: BoardTask
|
|
19
|
+
selectionMode?: boolean
|
|
20
|
+
selected?: boolean
|
|
21
|
+
onToggleSelect?: (id: string) => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function TaskCard({ task, selectionMode, selected, onToggleSelect }: TaskCardProps) {
|
|
18
25
|
const agents = useAppStore((s) => s.agents)
|
|
26
|
+
const projects = useAppStore((s) => s.projects)
|
|
19
27
|
const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
|
|
20
28
|
const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
|
|
21
29
|
const loadTasks = useAppStore((s) => s.loadTasks)
|
|
@@ -24,6 +32,7 @@ export function TaskCard({ task }: { task: BoardTask }) {
|
|
|
24
32
|
const [dragging, setDragging] = useState(false)
|
|
25
33
|
|
|
26
34
|
const agent = agents[task.agentId]
|
|
35
|
+
const project = task.projectId ? projects[task.projectId] : null
|
|
27
36
|
|
|
28
37
|
const isBlocked = Array.isArray(task.blockedBy) && task.blockedBy.length > 0
|
|
29
38
|
const isOverdue = task.dueAt && task.dueAt < Date.now() && task.status !== 'completed' && task.status !== 'archived'
|
|
@@ -65,17 +74,39 @@ export function TaskCard({ task }: { task: BoardTask }) {
|
|
|
65
74
|
|
|
66
75
|
return (
|
|
67
76
|
<div
|
|
68
|
-
draggable
|
|
69
|
-
onDragStart={handleDragStart}
|
|
70
|
-
onDragEnd={handleDragEnd}
|
|
71
|
-
onClick={() => {
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
draggable={!selectionMode}
|
|
78
|
+
onDragStart={selectionMode ? undefined : handleDragStart}
|
|
79
|
+
onDragEnd={selectionMode ? undefined : handleDragEnd}
|
|
80
|
+
onClick={(e) => {
|
|
81
|
+
if (selectionMode && onToggleSelect) {
|
|
82
|
+
e.stopPropagation()
|
|
83
|
+
onToggleSelect(task.id)
|
|
84
|
+
} else {
|
|
85
|
+
setEditingTaskId(task.id)
|
|
86
|
+
setTaskSheetOpen(true)
|
|
87
|
+
}
|
|
74
88
|
}}
|
|
75
|
-
className={`p-4 rounded-[14px] border border-
|
|
76
|
-
|
|
89
|
+
className={`p-4 rounded-[14px] border border-l-[3px] ${borderColor} bg-surface hover:bg-surface-2 transition-all group
|
|
90
|
+
${selectionMode ? 'cursor-pointer' : 'cursor-grab active:cursor-grabbing'}
|
|
91
|
+
${dragging ? 'opacity-40 scale-[0.97]' : ''}
|
|
92
|
+
${selected ? 'border-accent-bright/40 bg-accent-bright/[0.04] ring-1 ring-accent-bright/20' : 'border-white/[0.06]'}`}
|
|
77
93
|
>
|
|
78
94
|
<div className="flex items-start gap-3 mb-3">
|
|
95
|
+
{/* Selection checkbox */}
|
|
96
|
+
{(selectionMode || selected) && (
|
|
97
|
+
<button
|
|
98
|
+
onClick={(e) => { e.stopPropagation(); onToggleSelect?.(task.id) }}
|
|
99
|
+
className={`w-5 h-5 rounded-[6px] border-2 flex items-center justify-center shrink-0 mt-0.5 cursor-pointer transition-all
|
|
100
|
+
${selected
|
|
101
|
+
? 'bg-accent-bright border-accent-bright'
|
|
102
|
+
: 'bg-transparent border-white/[0.2] hover:border-white/[0.4]'}`}
|
|
103
|
+
style={{ padding: 0, fontFamily: 'inherit' }}
|
|
104
|
+
>
|
|
105
|
+
{selected && (
|
|
106
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="3" strokeLinecap="round"><path d="M20 6L9 17l-5-5" /></svg>
|
|
107
|
+
)}
|
|
108
|
+
</button>
|
|
109
|
+
)}
|
|
79
110
|
{isBlocked && (
|
|
80
111
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-rose-400 shrink-0 mt-0.5">
|
|
81
112
|
<title>{`Blocked by ${task.blockedBy?.length} task(s)`}</title>
|
|
@@ -152,6 +183,12 @@ export function TaskCard({ task }: { task: BoardTask }) {
|
|
|
152
183
|
{agent.name}
|
|
153
184
|
</span>
|
|
154
185
|
)}
|
|
186
|
+
{project && (
|
|
187
|
+
<span className="inline-flex items-center gap-1 px-2 py-1 rounded-[6px] bg-white/[0.04] text-text-2 text-[11px] font-600">
|
|
188
|
+
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: project.color || '#6366F1' }} />
|
|
189
|
+
{project.name}
|
|
190
|
+
</span>
|
|
191
|
+
)}
|
|
155
192
|
<span className="text-[11px] text-text-3">{timeAgo(task.updatedAt)}</span>
|
|
156
193
|
{task.comments && task.comments.length > 0 && (
|
|
157
194
|
<span className="flex items-center gap-1 text-[11px] text-text-3">
|