@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
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
6
|
+
import { updateAgent } from '@/lib/agents'
|
|
7
|
+
import { toast } from 'sonner'
|
|
8
|
+
import type { Agent, BoardTask, Schedule } from '@/types'
|
|
9
|
+
|
|
10
|
+
function relativeDate(ts: number): string {
|
|
11
|
+
const diff = Date.now() - ts
|
|
12
|
+
if (diff < 60_000) return 'just now'
|
|
13
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
|
|
14
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
|
|
15
|
+
if (diff < 604_800_000) return `${Math.floor(diff / 86_400_000)}d ago`
|
|
16
|
+
return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const STATUS_STYLES: Record<string, string> = {
|
|
20
|
+
backlog: 'bg-white/[0.06] text-text-3',
|
|
21
|
+
queued: 'bg-amber-500/15 text-amber-400',
|
|
22
|
+
running: 'bg-sky-500/15 text-sky-400',
|
|
23
|
+
completed: 'bg-emerald-500/15 text-emerald-400',
|
|
24
|
+
failed: 'bg-red-500/15 text-red-400',
|
|
25
|
+
archived: 'bg-white/[0.04] text-text-3/50',
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Inline picker to assign agents to a project */
|
|
29
|
+
function AssignAgentPicker({ projectId, onClose }: { projectId: string; onClose: () => void }) {
|
|
30
|
+
const agents = useAppStore((s) => s.agents) as Record<string, Agent>
|
|
31
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
32
|
+
const [query, setQuery] = useState('')
|
|
33
|
+
|
|
34
|
+
const unassigned = Object.values(agents).filter((a) =>
|
|
35
|
+
!a.trashedAt && a.projectId !== projectId && (!query || a.name.toLowerCase().includes(query.toLowerCase())),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
const handleAssign = async (agentId: string) => {
|
|
39
|
+
await updateAgent(agentId, { projectId })
|
|
40
|
+
await loadAgents()
|
|
41
|
+
toast.success('Agent assigned to project')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<>
|
|
46
|
+
<div className="fixed inset-0 z-40" onClick={onClose} />
|
|
47
|
+
<div className="absolute left-0 top-full mt-2 z-50 w-[260px] rounded-[12px] bg-[#1a1a2e]/95 backdrop-blur-xl border border-white/[0.1] shadow-[0_12px_40px_rgba(0,0,0,0.5)] overflow-hidden">
|
|
48
|
+
<div className="p-2.5 border-b border-white/[0.06]">
|
|
49
|
+
<input
|
|
50
|
+
value={query}
|
|
51
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
52
|
+
placeholder="Search agents..."
|
|
53
|
+
autoFocus
|
|
54
|
+
className="w-full px-2.5 py-1.5 text-[12px] bg-white/[0.06] rounded-[8px] border border-white/[0.08] text-text placeholder:text-text-3/50 outline-none"
|
|
55
|
+
style={{ fontFamily: 'inherit' }}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="max-h-[240px] overflow-y-auto p-1">
|
|
59
|
+
{unassigned.length === 0 && (
|
|
60
|
+
<div className="px-3 py-4 text-[11px] text-text-3/50 text-center">
|
|
61
|
+
{query ? 'No matching agents' : 'All agents are already assigned'}
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
{unassigned.map((a) => (
|
|
65
|
+
<button
|
|
66
|
+
key={a.id}
|
|
67
|
+
onClick={() => handleAssign(a.id)}
|
|
68
|
+
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-[8px] text-left hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none"
|
|
69
|
+
style={{ fontFamily: 'inherit' }}
|
|
70
|
+
>
|
|
71
|
+
<AgentAvatar seed={a.avatarSeed} name={a.name} size={22} />
|
|
72
|
+
<div className="min-w-0 flex-1">
|
|
73
|
+
<div className="text-[12px] text-text truncate">{a.name}</div>
|
|
74
|
+
<div className="text-[10px] text-text-3/40 truncate">{a.model || a.provider}</div>
|
|
75
|
+
</div>
|
|
76
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3/30 shrink-0">
|
|
77
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
78
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
79
|
+
</svg>
|
|
80
|
+
</button>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function ProjectDetail() {
|
|
89
|
+
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
90
|
+
const projects = useAppStore((s) => s.projects)
|
|
91
|
+
const agents = useAppStore((s) => s.agents) as Record<string, Agent>
|
|
92
|
+
const tasks = useAppStore((s) => s.tasks) as Record<string, BoardTask>
|
|
93
|
+
const schedules = useAppStore((s) => s.schedules) as Record<string, Schedule>
|
|
94
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
95
|
+
const setEditingProjectId = useAppStore((s) => s.setEditingProjectId)
|
|
96
|
+
const setProjectSheetOpen = useAppStore((s) => s.setProjectSheetOpen)
|
|
97
|
+
const setActiveView = useAppStore((s) => s.setActiveView)
|
|
98
|
+
const setCurrentAgent = useAppStore((s) => s.setCurrentAgent)
|
|
99
|
+
const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
|
|
100
|
+
const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
|
|
101
|
+
const setEditingScheduleId = useAppStore((s) => s.setEditingScheduleId)
|
|
102
|
+
const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
|
|
103
|
+
|
|
104
|
+
const [assignPickerOpen, setAssignPickerOpen] = useState(false)
|
|
105
|
+
|
|
106
|
+
const project = activeProjectFilter ? projects[activeProjectFilter] : null
|
|
107
|
+
|
|
108
|
+
const projectAgents = useMemo(
|
|
109
|
+
() => Object.values(agents).filter((a) => a.projectId === activeProjectFilter && !a.trashedAt),
|
|
110
|
+
[agents, activeProjectFilter],
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
const projectTasks = useMemo(
|
|
114
|
+
() => Object.values(tasks)
|
|
115
|
+
.filter((t) => t.projectId === activeProjectFilter)
|
|
116
|
+
.sort((a, b) => b.updatedAt - a.updatedAt),
|
|
117
|
+
[tasks, activeProjectFilter],
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const projectSchedules = useMemo(
|
|
121
|
+
() => Object.values(schedules).filter((s) => s.projectId === activeProjectFilter),
|
|
122
|
+
[schedules, activeProjectFilter],
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const completedTasks = projectTasks.filter((t) => t.status === 'completed').length
|
|
126
|
+
const totalTasks = projectTasks.length
|
|
127
|
+
const progressPct = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
|
|
128
|
+
|
|
129
|
+
// Task status breakdown for mini-chart
|
|
130
|
+
const tasksByStatus = useMemo(() => {
|
|
131
|
+
const counts: Record<string, number> = {}
|
|
132
|
+
for (const t of projectTasks) {
|
|
133
|
+
counts[t.status] = (counts[t.status] || 0) + 1
|
|
134
|
+
}
|
|
135
|
+
return counts
|
|
136
|
+
}, [projectTasks])
|
|
137
|
+
|
|
138
|
+
// Recent activity: merge tasks & schedules sorted by updatedAt
|
|
139
|
+
const recentActivity = useMemo(() => {
|
|
140
|
+
const items: { id: string; type: 'task' | 'schedule' | 'agent'; name: string; status?: string; time: number }[] = []
|
|
141
|
+
for (const t of projectTasks.slice(0, 10)) {
|
|
142
|
+
items.push({ id: t.id, type: 'task', name: t.title, status: t.status, time: t.updatedAt })
|
|
143
|
+
}
|
|
144
|
+
for (const s of projectSchedules) {
|
|
145
|
+
if (s.lastRunAt) items.push({ id: s.id, type: 'schedule', name: s.name, status: s.status, time: s.lastRunAt })
|
|
146
|
+
}
|
|
147
|
+
for (const a of projectAgents.slice(0, 5)) {
|
|
148
|
+
if (a.lastUsedAt) items.push({ id: a.id, type: 'agent', name: a.name, time: a.lastUsedAt })
|
|
149
|
+
}
|
|
150
|
+
return items.sort((a, b) => b.time - a.time).slice(0, 12)
|
|
151
|
+
}, [projectTasks, projectSchedules, projectAgents])
|
|
152
|
+
|
|
153
|
+
const handleUnassignAgent = async (agentId: string) => {
|
|
154
|
+
await updateAgent(agentId, { projectId: undefined })
|
|
155
|
+
await loadAgents()
|
|
156
|
+
toast.success('Agent removed from project')
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!project) {
|
|
160
|
+
return (
|
|
161
|
+
<div className="flex-1 flex items-center justify-center px-8">
|
|
162
|
+
<div className="text-center max-w-[420px]">
|
|
163
|
+
<div className="w-14 h-14 rounded-[16px] bg-white/[0.04] flex items-center justify-center mx-auto mb-4">
|
|
164
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/40">
|
|
165
|
+
<path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7-7H4a2 2 0 0 0-2 2v17Z" />
|
|
166
|
+
<path d="M14 2v7h7" />
|
|
167
|
+
</svg>
|
|
168
|
+
</div>
|
|
169
|
+
<h2 className="font-display text-[20px] font-700 text-text mb-2 tracking-[-0.02em]">
|
|
170
|
+
Select a Project
|
|
171
|
+
</h2>
|
|
172
|
+
<p className="text-[14px] text-text-3/60">
|
|
173
|
+
Choose a project from the list to see its agents, tasks, and activity.
|
|
174
|
+
</p>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<div className="flex-1 overflow-y-auto">
|
|
182
|
+
<div className="max-w-3xl mx-auto px-8 py-8">
|
|
183
|
+
|
|
184
|
+
{/* Project header */}
|
|
185
|
+
<div className="flex items-start gap-5 mb-8">
|
|
186
|
+
<div
|
|
187
|
+
className="w-12 h-12 rounded-[14px] flex items-center justify-center shrink-0 text-[20px] font-700 text-white/90"
|
|
188
|
+
style={{ backgroundColor: project.color || '#6366F1' }}
|
|
189
|
+
>
|
|
190
|
+
{project.name.charAt(0).toUpperCase()}
|
|
191
|
+
</div>
|
|
192
|
+
<div className="flex-1 min-w-0">
|
|
193
|
+
<div className="flex items-center gap-3">
|
|
194
|
+
<h1 className="font-display text-[24px] font-700 text-text tracking-[-0.02em] truncate">
|
|
195
|
+
{project.name}
|
|
196
|
+
</h1>
|
|
197
|
+
<button
|
|
198
|
+
onClick={() => { setEditingProjectId(project.id); setProjectSheetOpen(true) }}
|
|
199
|
+
className="shrink-0 p-1.5 rounded-[8px] hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none text-text-3/50 hover:text-text-2"
|
|
200
|
+
>
|
|
201
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
202
|
+
<path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
|
|
203
|
+
</svg>
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
{project.description && (
|
|
207
|
+
<p className="text-[14px] text-text-3/70 mt-1.5 leading-relaxed">{project.description}</p>
|
|
208
|
+
)}
|
|
209
|
+
<p className="text-[11px] text-text-3/40 mt-2">
|
|
210
|
+
Created {relativeDate(project.createdAt)} · Updated {relativeDate(project.updatedAt)}
|
|
211
|
+
</p>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Stats cards */}
|
|
216
|
+
<div className="grid grid-cols-4 gap-3 mb-8">
|
|
217
|
+
{[
|
|
218
|
+
{ label: 'Agents', value: projectAgents.length, color: '#818CF8' },
|
|
219
|
+
{ label: 'Tasks', value: totalTasks, color: project.color || '#6366F1' },
|
|
220
|
+
{ label: 'Completed', value: completedTasks, color: '#22C55E' },
|
|
221
|
+
{ label: 'Schedules', value: projectSchedules.length, color: '#F59E0B' },
|
|
222
|
+
].map((stat) => (
|
|
223
|
+
<div key={stat.label} className="rounded-[12px] border border-white/[0.06] bg-white/[0.02] px-4 py-3.5">
|
|
224
|
+
<div className="text-[22px] font-700 font-display tracking-[-0.02em]" style={{ color: stat.color }}>
|
|
225
|
+
{stat.value}
|
|
226
|
+
</div>
|
|
227
|
+
<div className="text-[11px] text-text-3/50 font-500 mt-0.5">{stat.label}</div>
|
|
228
|
+
</div>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
{/* Progress bar + task breakdown */}
|
|
233
|
+
{totalTasks > 0 && (
|
|
234
|
+
<div className="mb-8 rounded-[12px] border border-white/[0.06] bg-white/[0.02] px-5 py-4">
|
|
235
|
+
<div className="flex items-center justify-between mb-2.5">
|
|
236
|
+
<span className="text-[12px] font-600 text-text-2">Overall Progress</span>
|
|
237
|
+
<span className={`text-[13px] font-mono font-700 ${progressPct === 100 ? 'text-emerald-400' : 'text-text-2'}`}>
|
|
238
|
+
{progressPct}%
|
|
239
|
+
</span>
|
|
240
|
+
</div>
|
|
241
|
+
<div className="h-2 rounded-full bg-white/[0.06] overflow-hidden">
|
|
242
|
+
<div
|
|
243
|
+
className="h-full rounded-full transition-all duration-700"
|
|
244
|
+
style={{
|
|
245
|
+
width: `${progressPct}%`,
|
|
246
|
+
backgroundColor: progressPct === 100 ? '#22C55E' : (project.color || '#6366F1'),
|
|
247
|
+
}}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
<div className="flex items-center gap-4 mt-3 text-[10px] text-text-3/40">
|
|
251
|
+
{Object.entries(tasksByStatus).map(([status, count]) => (
|
|
252
|
+
<span key={status} className="flex items-center gap-1">
|
|
253
|
+
<span className={`w-1.5 h-1.5 rounded-full ${
|
|
254
|
+
status === 'completed' ? 'bg-emerald-400'
|
|
255
|
+
: status === 'running' ? 'bg-sky-400'
|
|
256
|
+
: status === 'queued' ? 'bg-amber-400'
|
|
257
|
+
: status === 'failed' ? 'bg-red-400'
|
|
258
|
+
: 'bg-white/[0.2]'
|
|
259
|
+
}`} />
|
|
260
|
+
{count} {status}
|
|
261
|
+
</span>
|
|
262
|
+
))}
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
|
|
267
|
+
{/* Agents section */}
|
|
268
|
+
<div className="mb-8">
|
|
269
|
+
<div className="flex items-center justify-between mb-3">
|
|
270
|
+
<h3 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">
|
|
271
|
+
Agents ({projectAgents.length})
|
|
272
|
+
</h3>
|
|
273
|
+
<div className="relative">
|
|
274
|
+
<button
|
|
275
|
+
onClick={() => setAssignPickerOpen(!assignPickerOpen)}
|
|
276
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 text-accent-bright bg-accent-soft hover:bg-accent-bright/15 transition-all cursor-pointer border-none"
|
|
277
|
+
style={{ fontFamily: 'inherit' }}
|
|
278
|
+
>
|
|
279
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
280
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
281
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
282
|
+
</svg>
|
|
283
|
+
Assign Agent
|
|
284
|
+
</button>
|
|
285
|
+
{assignPickerOpen && (
|
|
286
|
+
<AssignAgentPicker
|
|
287
|
+
projectId={project.id}
|
|
288
|
+
onClose={() => setAssignPickerOpen(false)}
|
|
289
|
+
/>
|
|
290
|
+
)}
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
{projectAgents.length === 0 ? (
|
|
294
|
+
<div className="rounded-[12px] border border-dashed border-white/[0.08] px-5 py-8 text-center">
|
|
295
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/30 mx-auto mb-2">
|
|
296
|
+
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
|
297
|
+
<circle cx="9" cy="7" r="4" />
|
|
298
|
+
<line x1="23" y1="11" x2="17" y2="11" />
|
|
299
|
+
</svg>
|
|
300
|
+
<p className="text-[12px] text-text-3/40">No agents assigned yet.</p>
|
|
301
|
+
<p className="text-[11px] text-text-3/30 mt-1">Click “Assign Agent” to add agents to this project.</p>
|
|
302
|
+
</div>
|
|
303
|
+
) : (
|
|
304
|
+
<div className="grid grid-cols-2 gap-2">
|
|
305
|
+
{projectAgents.map((agent) => (
|
|
306
|
+
<div
|
|
307
|
+
key={agent.id}
|
|
308
|
+
className="group/agent flex items-center gap-3 px-4 py-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] hover:bg-white/[0.05] hover:border-white/[0.1] transition-all"
|
|
309
|
+
>
|
|
310
|
+
<button
|
|
311
|
+
onClick={() => { setCurrentAgent(agent.id); setActiveView('agents') }}
|
|
312
|
+
className="flex items-center gap-3 flex-1 min-w-0 cursor-pointer bg-transparent border-none text-left p-0"
|
|
313
|
+
style={{ fontFamily: 'inherit' }}
|
|
314
|
+
>
|
|
315
|
+
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={28} />
|
|
316
|
+
<div className="min-w-0 flex-1">
|
|
317
|
+
<div className="text-[13px] font-600 text-text truncate">{agent.name}</div>
|
|
318
|
+
<div className="text-[11px] text-text-3/50 truncate">{agent.model || agent.provider}</div>
|
|
319
|
+
</div>
|
|
320
|
+
</button>
|
|
321
|
+
{agent.lastUsedAt && (
|
|
322
|
+
<span className="text-[10px] text-text-3/30 shrink-0">{relativeDate(agent.lastUsedAt)}</span>
|
|
323
|
+
)}
|
|
324
|
+
<button
|
|
325
|
+
onClick={() => handleUnassignAgent(agent.id)}
|
|
326
|
+
title="Remove from project"
|
|
327
|
+
className="opacity-0 group-hover/agent:opacity-100 p-1 rounded-[6px] hover:bg-red-500/10 text-text-3/30 hover:text-red-400 transition-all cursor-pointer bg-transparent border-none shrink-0"
|
|
328
|
+
>
|
|
329
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
330
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
331
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
332
|
+
</svg>
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
))}
|
|
336
|
+
</div>
|
|
337
|
+
)}
|
|
338
|
+
</div>
|
|
339
|
+
|
|
340
|
+
{/* Tasks section */}
|
|
341
|
+
<div className="mb-8">
|
|
342
|
+
<div className="flex items-center justify-between mb-3">
|
|
343
|
+
<h3 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">
|
|
344
|
+
Tasks ({totalTasks})
|
|
345
|
+
</h3>
|
|
346
|
+
<button
|
|
347
|
+
onClick={() => { setEditingTaskId(null); setTaskSheetOpen(true) }}
|
|
348
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 text-accent-bright bg-accent-soft hover:bg-accent-bright/15 transition-all cursor-pointer border-none"
|
|
349
|
+
style={{ fontFamily: 'inherit' }}
|
|
350
|
+
>
|
|
351
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
352
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
353
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
354
|
+
</svg>
|
|
355
|
+
New Task
|
|
356
|
+
</button>
|
|
357
|
+
</div>
|
|
358
|
+
{projectTasks.length === 0 ? (
|
|
359
|
+
<div className="rounded-[12px] border border-dashed border-white/[0.08] px-5 py-8 text-center">
|
|
360
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/30 mx-auto mb-2">
|
|
361
|
+
<path d="M9 11l3 3L22 4" />
|
|
362
|
+
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
|
|
363
|
+
</svg>
|
|
364
|
+
<p className="text-[12px] text-text-3/40">No tasks in this project yet.</p>
|
|
365
|
+
<p className="text-[11px] text-text-3/30 mt-1">Create tasks and assign them to this project from the task board.</p>
|
|
366
|
+
</div>
|
|
367
|
+
) : (
|
|
368
|
+
<div className="flex flex-col gap-1.5">
|
|
369
|
+
{projectTasks.slice(0, 10).map((task) => {
|
|
370
|
+
const agent = task.agentId ? agents[task.agentId] : null
|
|
371
|
+
return (
|
|
372
|
+
<button
|
|
373
|
+
key={task.id}
|
|
374
|
+
onClick={() => { setEditingTaskId(task.id); setTaskSheetOpen(true) }}
|
|
375
|
+
className="flex items-center gap-3 px-4 py-3 rounded-[10px] border border-white/[0.04] bg-white/[0.01] hover:bg-white/[0.04] hover:border-white/[0.08] transition-all cursor-pointer text-left w-full"
|
|
376
|
+
style={{ fontFamily: 'inherit' }}
|
|
377
|
+
>
|
|
378
|
+
<span className={`shrink-0 px-2 py-0.5 rounded-[5px] text-[10px] font-600 uppercase tracking-wider ${STATUS_STYLES[task.status] || STATUS_STYLES.backlog}`}>
|
|
379
|
+
{task.status}
|
|
380
|
+
</span>
|
|
381
|
+
<span className="text-[13px] text-text truncate flex-1">{task.title}</span>
|
|
382
|
+
{agent && (
|
|
383
|
+
<span className="shrink-0 flex items-center gap-1.5 text-[11px] text-text-3/40">
|
|
384
|
+
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={16} />
|
|
385
|
+
{agent.name}
|
|
386
|
+
</span>
|
|
387
|
+
)}
|
|
388
|
+
<span className="text-[10px] text-text-3/30 shrink-0">{relativeDate(task.updatedAt)}</span>
|
|
389
|
+
</button>
|
|
390
|
+
)
|
|
391
|
+
})}
|
|
392
|
+
{projectTasks.length > 10 && (
|
|
393
|
+
<button
|
|
394
|
+
onClick={() => setActiveView('tasks')}
|
|
395
|
+
className="text-[11px] text-accent-bright/70 hover:text-accent-bright text-center py-2 cursor-pointer bg-transparent border-none transition-colors"
|
|
396
|
+
style={{ fontFamily: 'inherit' }}
|
|
397
|
+
>
|
|
398
|
+
View all {projectTasks.length} tasks on board
|
|
399
|
+
</button>
|
|
400
|
+
)}
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
{/* Schedules section */}
|
|
406
|
+
{(projectSchedules.length > 0 || projectAgents.length > 0) && (
|
|
407
|
+
<div className="mb-8">
|
|
408
|
+
<div className="flex items-center justify-between mb-3">
|
|
409
|
+
<h3 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">
|
|
410
|
+
Schedules ({projectSchedules.length})
|
|
411
|
+
</h3>
|
|
412
|
+
<button
|
|
413
|
+
onClick={() => { setEditingScheduleId(null); setScheduleSheetOpen(true) }}
|
|
414
|
+
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 text-accent-bright bg-accent-soft hover:bg-accent-bright/15 transition-all cursor-pointer border-none"
|
|
415
|
+
style={{ fontFamily: 'inherit' }}
|
|
416
|
+
>
|
|
417
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
418
|
+
<line x1="12" y1="5" x2="12" y2="19" />
|
|
419
|
+
<line x1="5" y1="12" x2="19" y2="12" />
|
|
420
|
+
</svg>
|
|
421
|
+
New Schedule
|
|
422
|
+
</button>
|
|
423
|
+
</div>
|
|
424
|
+
{projectSchedules.length === 0 ? (
|
|
425
|
+
<div className="rounded-[12px] border border-dashed border-white/[0.08] px-5 py-6 text-center">
|
|
426
|
+
<p className="text-[12px] text-text-3/40">No schedules yet.</p>
|
|
427
|
+
</div>
|
|
428
|
+
) : (
|
|
429
|
+
<div className="flex flex-col gap-1.5">
|
|
430
|
+
{projectSchedules.map((schedule) => {
|
|
431
|
+
const agent = schedule.agentId ? agents[schedule.agentId] : null
|
|
432
|
+
return (
|
|
433
|
+
<button
|
|
434
|
+
key={schedule.id}
|
|
435
|
+
onClick={() => { setEditingScheduleId(schedule.id); setScheduleSheetOpen(true) }}
|
|
436
|
+
className="flex items-center gap-3 px-4 py-3 rounded-[10px] border border-white/[0.04] bg-white/[0.01] hover:bg-white/[0.04] hover:border-white/[0.08] transition-all cursor-pointer text-left w-full"
|
|
437
|
+
style={{ fontFamily: 'inherit' }}
|
|
438
|
+
>
|
|
439
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400/60 shrink-0">
|
|
440
|
+
<circle cx="12" cy="12" r="10" />
|
|
441
|
+
<polyline points="12 6 12 12 16 14" />
|
|
442
|
+
</svg>
|
|
443
|
+
<span className="text-[13px] text-text truncate flex-1">{schedule.name}</span>
|
|
444
|
+
<span className={`shrink-0 px-2 py-0.5 rounded-[5px] text-[10px] font-600 uppercase tracking-wider ${
|
|
445
|
+
schedule.status === 'active' ? 'bg-emerald-500/15 text-emerald-400' : 'bg-white/[0.06] text-text-3'
|
|
446
|
+
}`}>
|
|
447
|
+
{schedule.status}
|
|
448
|
+
</span>
|
|
449
|
+
{agent && (
|
|
450
|
+
<span className="shrink-0 flex items-center gap-1.5 text-[11px] text-text-3/40">
|
|
451
|
+
<AgentAvatar seed={agent.avatarSeed} name={agent.name} size={16} />
|
|
452
|
+
</span>
|
|
453
|
+
)}
|
|
454
|
+
{schedule.nextRunAt && (
|
|
455
|
+
<span className="text-[10px] text-text-3/30 shrink-0">
|
|
456
|
+
next: {relativeDate(schedule.nextRunAt)}
|
|
457
|
+
</span>
|
|
458
|
+
)}
|
|
459
|
+
</button>
|
|
460
|
+
)
|
|
461
|
+
})}
|
|
462
|
+
</div>
|
|
463
|
+
)}
|
|
464
|
+
</div>
|
|
465
|
+
)}
|
|
466
|
+
|
|
467
|
+
{/* Recent activity */}
|
|
468
|
+
{recentActivity.length > 0 && (
|
|
469
|
+
<div className="mb-8">
|
|
470
|
+
<h3 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60 mb-3">
|
|
471
|
+
Recent Activity
|
|
472
|
+
</h3>
|
|
473
|
+
<div className="relative pl-5">
|
|
474
|
+
<div className="absolute left-[7px] top-2 bottom-2 w-px bg-white/[0.06]" />
|
|
475
|
+
<div className="flex flex-col gap-3">
|
|
476
|
+
{recentActivity.map((item) => (
|
|
477
|
+
<div key={`${item.type}-${item.id}`} className="relative flex items-start gap-3">
|
|
478
|
+
<div className={`absolute left-[-13px] top-1.5 w-2 h-2 rounded-full ${
|
|
479
|
+
item.type === 'task' && item.status === 'completed' ? 'bg-emerald-400'
|
|
480
|
+
: item.type === 'task' && item.status === 'running' ? 'bg-sky-400'
|
|
481
|
+
: item.type === 'task' && item.status === 'failed' ? 'bg-red-400'
|
|
482
|
+
: item.type === 'schedule' ? 'bg-amber-400'
|
|
483
|
+
: 'bg-white/[0.2]'
|
|
484
|
+
}`} />
|
|
485
|
+
<div className="flex-1 min-w-0">
|
|
486
|
+
<div className="flex items-center gap-2">
|
|
487
|
+
<span className="text-[10px] font-600 uppercase tracking-wider text-text-3/40">
|
|
488
|
+
{item.type}
|
|
489
|
+
</span>
|
|
490
|
+
{item.status && (
|
|
491
|
+
<span className={`text-[9px] font-600 uppercase tracking-wider px-1.5 py-0.5 rounded-[4px] ${STATUS_STYLES[item.status] || 'bg-white/[0.06] text-text-3'}`}>
|
|
492
|
+
{item.status}
|
|
493
|
+
</span>
|
|
494
|
+
)}
|
|
495
|
+
</div>
|
|
496
|
+
<p className="text-[12px] text-text-2 truncate mt-0.5">{item.name}</p>
|
|
497
|
+
</div>
|
|
498
|
+
<span className="text-[10px] text-text-3/30 shrink-0 mt-0.5">{relativeDate(item.time)}</span>
|
|
499
|
+
</div>
|
|
500
|
+
))}
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
)}
|
|
505
|
+
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
)
|
|
509
|
+
}
|