@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.
Files changed (173) hide show
  1. package/README.md +42 -7
  2. package/bin/swarmclaw.js +76 -16
  3. package/next.config.ts +11 -1
  4. package/package.json +4 -2
  5. package/public/screenshots/agents.png +0 -0
  6. package/public/screenshots/dashboard.png +0 -0
  7. package/public/screenshots/providers.png +0 -0
  8. package/public/screenshots/tasks.png +0 -0
  9. package/scripts/postinstall.mjs +18 -0
  10. package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
  11. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  12. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  13. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  14. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  15. package/src/app/api/chatrooms/route.ts +50 -0
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/knowledge/[id]/route.ts +13 -2
  18. package/src/app/api/knowledge/route.ts +8 -1
  19. package/src/app/api/memory/route.ts +8 -0
  20. package/src/app/api/notifications/[id]/route.ts +27 -0
  21. package/src/app/api/notifications/route.ts +68 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +155 -0
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/route.ts +3 -3
  29. package/src/app/api/settings/route.ts +9 -0
  30. package/src/app/api/setup/check-provider/route.ts +3 -16
  31. package/src/app/api/skills/[id]/route.ts +6 -0
  32. package/src/app/api/skills/route.ts +6 -0
  33. package/src/app/api/tasks/[id]/route.ts +20 -0
  34. package/src/app/api/tasks/bulk/route.ts +100 -0
  35. package/src/app/api/tasks/route.ts +1 -0
  36. package/src/app/api/usage/route.ts +45 -0
  37. package/src/app/api/webhooks/[id]/route.ts +15 -1
  38. package/src/app/globals.css +58 -15
  39. package/src/app/page.tsx +142 -13
  40. package/src/cli/index.js +42 -0
  41. package/src/cli/index.test.js +30 -0
  42. package/src/cli/spec.js +32 -0
  43. package/src/components/agents/agent-avatar.tsx +57 -10
  44. package/src/components/agents/agent-card.tsx +48 -15
  45. package/src/components/agents/agent-chat-list.tsx +123 -10
  46. package/src/components/agents/agent-list.tsx +50 -19
  47. package/src/components/agents/agent-sheet.tsx +56 -63
  48. package/src/components/auth/access-key-gate.tsx +10 -3
  49. package/src/components/auth/setup-wizard.tsx +2 -2
  50. package/src/components/auth/user-picker.tsx +31 -3
  51. package/src/components/chat/activity-moment.tsx +169 -0
  52. package/src/components/chat/chat-header.tsx +2 -0
  53. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  54. package/src/components/chat/file-path-chip.tsx +125 -0
  55. package/src/components/chat/markdown-utils.ts +9 -0
  56. package/src/components/chat/message-bubble.tsx +46 -295
  57. package/src/components/chat/message-list.tsx +50 -1
  58. package/src/components/chat/streaming-bubble.tsx +36 -46
  59. package/src/components/chat/suggestions-bar.tsx +1 -1
  60. package/src/components/chat/thinking-indicator.tsx +72 -10
  61. package/src/components/chat/tool-call-bubble.tsx +66 -70
  62. package/src/components/chat/tool-request-banner.tsx +31 -7
  63. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  64. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  65. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  66. package/src/components/chatrooms/chatroom-list.tsx +123 -0
  67. package/src/components/chatrooms/chatroom-message.tsx +427 -0
  68. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  69. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  70. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  71. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  72. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  73. package/src/components/connectors/connector-sheet.tsx +34 -47
  74. package/src/components/home/home-view.tsx +501 -0
  75. package/src/components/input/chat-input.tsx +79 -41
  76. package/src/components/knowledge/knowledge-list.tsx +31 -1
  77. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  78. package/src/components/layout/app-layout.tsx +209 -83
  79. package/src/components/layout/mobile-header.tsx +2 -0
  80. package/src/components/layout/update-banner.tsx +2 -2
  81. package/src/components/logs/log-list.tsx +2 -2
  82. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  83. package/src/components/memory/memory-agent-list.tsx +143 -0
  84. package/src/components/memory/memory-browser.tsx +205 -0
  85. package/src/components/memory/memory-card.tsx +34 -7
  86. package/src/components/memory/memory-detail.tsx +359 -120
  87. package/src/components/memory/memory-sheet.tsx +157 -23
  88. package/src/components/plugins/plugin-list.tsx +1 -1
  89. package/src/components/plugins/plugin-sheet.tsx +1 -1
  90. package/src/components/projects/project-detail.tsx +509 -0
  91. package/src/components/projects/project-list.tsx +195 -59
  92. package/src/components/providers/provider-list.tsx +2 -2
  93. package/src/components/providers/provider-sheet.tsx +3 -3
  94. package/src/components/schedules/schedule-card.tsx +3 -2
  95. package/src/components/schedules/schedule-list.tsx +1 -1
  96. package/src/components/schedules/schedule-sheet.tsx +25 -25
  97. package/src/components/secrets/secret-sheet.tsx +47 -24
  98. package/src/components/secrets/secrets-list.tsx +18 -8
  99. package/src/components/sessions/new-session-sheet.tsx +33 -65
  100. package/src/components/sessions/session-card.tsx +45 -14
  101. package/src/components/sessions/session-list.tsx +35 -18
  102. package/src/components/shared/agent-picker-list.tsx +90 -0
  103. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  104. package/src/components/shared/attachment-chip.tsx +165 -0
  105. package/src/components/shared/avatar.tsx +10 -1
  106. package/src/components/shared/check-icon.tsx +12 -0
  107. package/src/components/shared/confirm-dialog.tsx +1 -1
  108. package/src/components/shared/empty-state.tsx +32 -0
  109. package/src/components/shared/file-preview.tsx +34 -0
  110. package/src/components/shared/form-styles.ts +2 -0
  111. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  112. package/src/components/shared/notification-center.tsx +223 -0
  113. package/src/components/shared/profile-sheet.tsx +115 -0
  114. package/src/components/shared/reply-quote.tsx +26 -0
  115. package/src/components/shared/search-dialog.tsx +296 -0
  116. package/src/components/shared/section-label.tsx +12 -0
  117. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  118. package/src/components/shared/settings/section-providers.tsx +1 -1
  119. package/src/components/shared/settings/section-secrets.tsx +1 -1
  120. package/src/components/shared/settings/section-theme.tsx +95 -0
  121. package/src/components/shared/settings/section-user-preferences.tsx +39 -0
  122. package/src/components/shared/settings/settings-page.tsx +180 -27
  123. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  124. package/src/components/shared/sheet-footer.tsx +33 -0
  125. package/src/components/skills/skill-list.tsx +61 -30
  126. package/src/components/skills/skill-sheet.tsx +81 -2
  127. package/src/components/tasks/task-board.tsx +448 -26
  128. package/src/components/tasks/task-card.tsx +46 -9
  129. package/src/components/tasks/task-column.tsx +62 -3
  130. package/src/components/tasks/task-list.tsx +12 -4
  131. package/src/components/tasks/task-sheet.tsx +89 -72
  132. package/src/components/ui/hover-card.tsx +52 -0
  133. package/src/components/usage/metrics-dashboard.tsx +78 -0
  134. package/src/components/usage/usage-list.tsx +1 -1
  135. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  136. package/src/hooks/use-view-router.ts +69 -19
  137. package/src/instrumentation.ts +15 -1
  138. package/src/lib/chat.ts +2 -0
  139. package/src/lib/cron-human.ts +114 -0
  140. package/src/lib/memory.ts +3 -0
  141. package/src/lib/server/chat-execution.ts +24 -4
  142. package/src/lib/server/connectors/manager.ts +11 -0
  143. package/src/lib/server/context-manager.ts +225 -13
  144. package/src/lib/server/create-notification.ts +42 -0
  145. package/src/lib/server/daemon-state.ts +165 -10
  146. package/src/lib/server/execution-log.ts +1 -0
  147. package/src/lib/server/heartbeat-service.ts +40 -5
  148. package/src/lib/server/heartbeat-wake.ts +110 -0
  149. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  150. package/src/lib/server/memory-consolidation.ts +92 -0
  151. package/src/lib/server/memory-db.ts +51 -6
  152. package/src/lib/server/openclaw-gateway.ts +9 -1
  153. package/src/lib/server/provider-health.ts +125 -0
  154. package/src/lib/server/queue.ts +5 -4
  155. package/src/lib/server/scheduler.ts +8 -0
  156. package/src/lib/server/session-run-manager.ts +4 -0
  157. package/src/lib/server/session-tools/chatroom.ts +136 -0
  158. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  159. package/src/lib/server/session-tools/index.ts +2 -0
  160. package/src/lib/server/session-tools/memory.ts +6 -1
  161. package/src/lib/server/storage.ts +80 -29
  162. package/src/lib/server/stream-agent-chat.ts +153 -47
  163. package/src/lib/server/system-events.ts +49 -0
  164. package/src/lib/server/ws-hub.ts +11 -0
  165. package/src/lib/soul-suggestions.ts +109 -0
  166. package/src/lib/tasks.ts +4 -1
  167. package/src/lib/view-routes.ts +36 -1
  168. package/src/lib/ws-client.ts +14 -4
  169. package/src/proxy.ts +79 -2
  170. package/src/stores/use-app-store.ts +94 -3
  171. package/src/stores/use-chat-store.ts +48 -3
  172. package/src/stores/use-chatroom-store.ts +276 -0
  173. package/src/types/index.ts +69 -2
@@ -2,20 +2,45 @@
2
2
 
3
3
  import { useEffect, useMemo, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import type { Agent, BoardTask, Schedule } from '@/types'
6
+
7
+ function relativeDate(ts: number): string {
8
+ const diff = Date.now() - ts
9
+ if (diff < 60_000) return 'just now'
10
+ if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`
11
+ if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`
12
+ if (diff < 604_800_000) return `${Math.floor(diff / 86_400_000)}d ago`
13
+ return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
14
+ }
15
+
16
+ interface ProjectStats {
17
+ agents: number
18
+ tasks: number
19
+ completedTasks: number
20
+ schedules: number
21
+ lastActivity: number
22
+ }
5
23
 
6
24
  export function ProjectList() {
7
25
  const projects = useAppStore((s) => s.projects)
8
26
  const loadProjects = useAppStore((s) => s.loadProjects)
9
- const agents = useAppStore((s) => s.agents)
10
- const tasks = useAppStore((s) => s.tasks)
27
+ const agents = useAppStore((s) => s.agents) as Record<string, Agent>
28
+ const tasks = useAppStore((s) => s.tasks) as Record<string, BoardTask>
29
+ const schedules = useAppStore((s) => s.schedules) as Record<string, Schedule>
11
30
  const setProjectSheetOpen = useAppStore((s) => s.setProjectSheetOpen)
12
31
  const setEditingProjectId = useAppStore((s) => s.setEditingProjectId)
13
32
  const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
14
33
  const setActiveProjectFilter = useAppStore((s) => s.setActiveProjectFilter)
34
+ const loadTasks = useAppStore((s) => s.loadTasks)
35
+ const loadSchedules = useAppStore((s) => s.loadSchedules)
15
36
  const [search, setSearch] = useState('')
16
37
 
17
- // eslint-disable-next-line react-hooks/exhaustive-deps
18
- useEffect(() => { loadProjects() }, [])
38
+ useEffect(() => {
39
+ loadProjects()
40
+ loadTasks()
41
+ loadSchedules()
42
+ // eslint-disable-next-line react-hooks/exhaustive-deps
43
+ }, [])
19
44
 
20
45
  const filtered = useMemo(() => {
21
46
  return Object.values(projects)
@@ -26,97 +51,208 @@ export function ProjectList() {
26
51
  .sort((a, b) => b.updatedAt - a.updatedAt)
27
52
  }, [projects, search])
28
53
 
29
- const entityCounts = useMemo(() => {
30
- const counts: Record<string, { agents: number; tasks: number }> = {}
54
+ const statsMap = useMemo(() => {
55
+ const map: Record<string, ProjectStats> = {}
31
56
  for (const p of Object.values(projects)) {
32
- counts[p.id] = { agents: 0, tasks: 0 }
57
+ map[p.id] = { agents: 0, tasks: 0, completedTasks: 0, schedules: 0, lastActivity: p.updatedAt }
33
58
  }
34
59
  for (const a of Object.values(agents)) {
35
- if (a.projectId && counts[a.projectId]) counts[a.projectId].agents++
60
+ if (a.projectId && map[a.projectId]) {
61
+ map[a.projectId].agents++
62
+ if (a.updatedAt && a.updatedAt > map[a.projectId].lastActivity) {
63
+ map[a.projectId].lastActivity = a.updatedAt
64
+ }
65
+ }
36
66
  }
37
67
  for (const t of Object.values(tasks)) {
38
- if (t.projectId && counts[t.projectId]) counts[t.projectId].tasks++
68
+ if (t.projectId && map[t.projectId]) {
69
+ map[t.projectId].tasks++
70
+ if (t.status === 'completed') map[t.projectId].completedTasks++
71
+ if (t.updatedAt && t.updatedAt > map[t.projectId].lastActivity) {
72
+ map[t.projectId].lastActivity = t.updatedAt
73
+ }
74
+ }
75
+ }
76
+ for (const s of Object.values(schedules)) {
77
+ if (s.projectId && map[s.projectId]) {
78
+ map[s.projectId].schedules++
79
+ }
39
80
  }
40
- return counts
41
- }, [projects, agents, tasks])
81
+ return map
82
+ }, [projects, agents, tasks, schedules])
83
+
84
+ // Summary stats
85
+ const totalProjects = Object.keys(projects).length
86
+ const totalTasks = Object.values(tasks).filter((t) => t.projectId).length
87
+ const totalCompleted = Object.values(tasks).filter((t) => t.projectId && t.status === 'completed').length
42
88
 
43
89
  if (!filtered.length && !search) {
44
90
  return (
45
91
  <div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
46
- <div className="w-12 h-12 rounded-[14px] bg-accent-soft flex items-center justify-center mb-1">
47
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright">
92
+ <div className="w-14 h-14 rounded-[16px] bg-accent-soft flex items-center justify-center mb-1">
93
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-accent-bright">
48
94
  <path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7-7H4a2 2 0 0 0-2 2v17Z" />
49
95
  <path d="M14 2v7h7" />
50
96
  </svg>
51
97
  </div>
52
- <p className="font-display text-[15px] font-600 text-text-2">No projects yet</p>
53
- <p className="text-[13px] text-text-3/50">Group agents, tasks, and schedules into projects</p>
98
+ <p className="font-display text-[16px] font-600 text-text-2">No projects yet</p>
99
+ <p className="text-[13px] text-text-3/60 max-w-[280px]">
100
+ Projects group your agents, tasks, and schedules together. Create one to get organized.
101
+ </p>
54
102
  <button
55
103
  onClick={() => { setEditingProjectId(null); setProjectSheetOpen(true) }}
56
- className="inline-flex items-center gap-1.5 px-4 py-2 text-[13px] font-500 text-white bg-accent rounded-lg hover:bg-accent-bright transition-colors"
104
+ className="inline-flex items-center gap-1.5 px-5 py-2.5 text-[13px] font-600 text-white bg-accent-bright rounded-[10px] hover:brightness-110 transition-all cursor-pointer border-none"
105
+ style={{ fontFamily: 'inherit' }}
57
106
  >
58
- <span className="text-lg leading-none">+</span> New Project
107
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
108
+ <line x1="12" y1="5" x2="12" y2="19" />
109
+ <line x1="5" y1="12" x2="19" y2="12" />
110
+ </svg>
111
+ New Project
59
112
  </button>
60
113
  </div>
61
114
  )
62
115
  }
63
116
 
64
117
  return (
65
- <div className="flex-1 flex flex-col h-full overflow-y-auto">
66
- <div className="p-4 pb-0">
67
- <div className="flex items-center gap-2 mb-4">
118
+ <div className="flex-1 flex flex-col h-full overflow-hidden">
119
+ {/* Header with search and new button */}
120
+ <div className="px-5 pt-5 pb-3 shrink-0">
121
+ <div className="flex items-center justify-between mb-4">
122
+ <div>
123
+ <h2 className="font-display text-[20px] font-700 text-text tracking-[-0.02em]">Projects</h2>
124
+ <p className="text-[12px] text-text-3/60 mt-0.5">
125
+ {totalProjects} project{totalProjects !== 1 ? 's' : ''}
126
+ {totalTasks > 0 && <> &middot; {totalCompleted}/{totalTasks} tasks done</>}
127
+ </p>
128
+ </div>
129
+ <button
130
+ onClick={() => { setEditingProjectId(null); setProjectSheetOpen(true) }}
131
+ className="inline-flex items-center gap-1.5 px-3.5 py-2 text-[12px] font-600 text-white bg-accent-bright rounded-[10px] hover:brightness-110 transition-all cursor-pointer border-none"
132
+ style={{ fontFamily: 'inherit' }}
133
+ >
134
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
135
+ <line x1="12" y1="5" x2="12" y2="19" />
136
+ <line x1="5" y1="12" x2="19" y2="12" />
137
+ </svg>
138
+ New
139
+ </button>
140
+ </div>
141
+
142
+ {/* Search */}
143
+ <div className="relative">
144
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="absolute left-3 top-1/2 -translate-y-1/2 text-text-3/50">
145
+ <circle cx="11" cy="11" r="8" />
146
+ <line x1="21" y1="21" x2="16.65" y2="16.65" />
147
+ </svg>
68
148
  <input
69
149
  type="text"
70
150
  value={search}
71
151
  onChange={(e) => setSearch(e.target.value)}
72
152
  placeholder="Search projects..."
73
- className="flex-1 px-3 py-2 rounded-lg bg-white/[0.06] border border-white/[0.06] text-[13px] text-text-1 placeholder:text-text-3/40 focus:outline-none focus:border-accent/40"
153
+ className="w-full pl-9 pr-3 py-2.5 rounded-[10px] bg-white/[0.04] border border-white/[0.06] text-[13px] text-text placeholder:text-text-3/40 focus:outline-none focus:border-accent-bright/30 transition-colors"
74
154
  style={{ fontFamily: 'inherit' }}
75
155
  />
76
156
  </div>
77
157
  </div>
78
- <div className="flex-1 overflow-y-auto px-4 pb-4 space-y-2">
79
- {filtered.map((project) => {
80
- const counts = entityCounts[project.id] || { agents: 0, tasks: 0 }
81
- const isActive = activeProjectFilter === project.id
82
- return (
83
- <div
84
- key={project.id}
85
- className={`group relative p-4 rounded-xl border transition-colors cursor-pointer ${
86
- isActive
87
- ? 'bg-accent/10 border-accent/30'
88
- : 'bg-white/[0.03] border-white/[0.06] hover:bg-white/[0.06]'
89
- }`}
90
- onClick={() => setActiveProjectFilter(isActive ? null : project.id)}
91
- >
92
- <div className="flex items-start justify-between gap-3">
93
- <div className="flex items-center gap-2.5 min-w-0">
94
- {project.color && (
95
- <div className="w-3 h-3 rounded-full shrink-0" style={{ backgroundColor: project.color }} />
96
- )}
97
- <div className="min-w-0">
98
- <div className="font-display text-[14px] font-600 text-text-1 truncate">{project.name}</div>
99
- {project.description && (
100
- <p className="text-[12px] text-text-3/60 mt-0.5 line-clamp-2">{project.description}</p>
158
+
159
+ {/* Project cards */}
160
+ <div className="flex-1 overflow-y-auto px-5 pb-5">
161
+ <div className="grid gap-3">
162
+ {filtered.map((project) => {
163
+ const stats = statsMap[project.id] || { agents: 0, tasks: 0, completedTasks: 0, schedules: 0, lastActivity: project.updatedAt }
164
+ const isActive = activeProjectFilter === project.id
165
+ const progressPct = stats.tasks > 0 ? Math.round((stats.completedTasks / stats.tasks) * 100) : 0
166
+
167
+ return (
168
+ <div
169
+ key={project.id}
170
+ className={`group relative rounded-[14px] border transition-all duration-200 cursor-pointer overflow-hidden
171
+ ${isActive
172
+ ? 'bg-white/[0.06] border-accent-bright/30 shadow-[0_0_20px_rgba(99,102,241,0.08)]'
173
+ : 'bg-white/[0.02] border-white/[0.06] hover:bg-white/[0.05] hover:border-white/[0.1]'}`}
174
+ onClick={() => setActiveProjectFilter(isActive ? null : project.id)}
175
+ >
176
+ {/* Color accent stripe */}
177
+ <div className="absolute left-0 top-0 bottom-0 w-1 rounded-l-[14px]" style={{ backgroundColor: project.color || '#6B7280' }} />
178
+
179
+ <div className="pl-5 pr-4 py-4">
180
+ <div className="flex items-start justify-between gap-3">
181
+ <div className="min-w-0 flex-1">
182
+ <div className="flex items-center gap-2">
183
+ <h3 className="font-display text-[14px] font-600 text-text truncate">{project.name}</h3>
184
+ {isActive && (
185
+ <span className="shrink-0 text-[9px] font-700 uppercase tracking-wider text-accent-bright bg-accent-soft px-1.5 py-0.5 rounded-[5px]">
186
+ active filter
187
+ </span>
188
+ )}
189
+ </div>
190
+ {project.description && (
191
+ <p className="text-[12px] text-text-3/60 mt-1 line-clamp-2 leading-relaxed">{project.description}</p>
192
+ )}
193
+ </div>
194
+ <button
195
+ onClick={(e) => { e.stopPropagation(); setEditingProjectId(project.id); setProjectSheetOpen(true) }}
196
+ className="opacity-0 group-hover:opacity-100 p-1.5 rounded-[8px] hover:bg-white/[0.08] transition-all text-text-3/50 hover:text-text-2 cursor-pointer bg-transparent border-none shrink-0"
197
+ >
198
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
199
+ <path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
200
+ </svg>
201
+ </button>
202
+ </div>
203
+
204
+ {/* Stats row */}
205
+ <div className="flex items-center gap-4 mt-3 text-[11px] text-text-3/50">
206
+ <span className="flex items-center gap-1.5">
207
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
208
+ <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
209
+ <circle cx="9" cy="7" r="4" />
210
+ </svg>
211
+ {stats.agents} agent{stats.agents !== 1 ? 's' : ''}
212
+ </span>
213
+ <span className="flex items-center gap-1.5">
214
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
215
+ <path d="M9 11l3 3L22 4" />
216
+ <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
217
+ </svg>
218
+ {stats.completedTasks}/{stats.tasks} task{stats.tasks !== 1 ? 's' : ''}
219
+ </span>
220
+ {stats.schedules > 0 && (
221
+ <span className="flex items-center gap-1.5">
222
+ <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
223
+ <circle cx="12" cy="12" r="10" />
224
+ <polyline points="12 6 12 12 16 14" />
225
+ </svg>
226
+ {stats.schedules} schedule{stats.schedules !== 1 ? 's' : ''}
227
+ </span>
101
228
  )}
229
+ <span className="ml-auto text-text-3/40">
230
+ {relativeDate(stats.lastActivity)}
231
+ </span>
102
232
  </div>
233
+
234
+ {/* Progress bar */}
235
+ {stats.tasks > 0 && (
236
+ <div className="mt-3 flex items-center gap-2.5">
237
+ <div className="flex-1 h-1.5 rounded-full bg-white/[0.06] overflow-hidden">
238
+ <div
239
+ className="h-full rounded-full transition-all duration-500"
240
+ style={{
241
+ width: `${progressPct}%`,
242
+ backgroundColor: progressPct === 100 ? '#22C55E' : (project.color || '#6366F1'),
243
+ }}
244
+ />
245
+ </div>
246
+ <span className={`text-[10px] font-mono font-600 ${progressPct === 100 ? 'text-emerald-400' : 'text-text-3/50'}`}>
247
+ {progressPct}%
248
+ </span>
249
+ </div>
250
+ )}
103
251
  </div>
104
- <button
105
- onClick={(e) => { e.stopPropagation(); setEditingProjectId(project.id); setProjectSheetOpen(true) }}
106
- className="opacity-0 group-hover:opacity-100 p-1.5 rounded-md hover:bg-white/[0.08] transition-all text-text-3/50 hover:text-text-2"
107
- >
108
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
109
- <path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
110
- </svg>
111
- </button>
112
252
  </div>
113
- <div className="flex items-center gap-3 mt-2.5 text-[11px] text-text-3/50">
114
- <span>{counts.agents} agent{counts.agents !== 1 ? 's' : ''}</span>
115
- <span>{counts.tasks} task{counts.tasks !== 1 ? 's' : ''}</span>
116
- </div>
117
- </div>
118
- )
119
- })}
253
+ )
254
+ })}
255
+ </div>
120
256
  </div>
121
257
  </div>
122
258
  )
@@ -86,7 +86,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
86
86
  <span className="font-display text-[14px] font-600 text-text truncate">{item.name}</span>
87
87
  <div className="flex items-center gap-2 shrink-0">
88
88
  <span className={`text-[10px] font-600 px-2 py-0.5 rounded-[5px] uppercase tracking-wider
89
- ${item.type === 'builtin' ? 'bg-white/[0.04] text-text-3' : 'bg-[#6366F1]/10 text-[#6366F1]'}`}>
89
+ ${item.type === 'builtin' ? 'bg-white/[0.04] text-text-3' : 'bg-accent-bright/10 text-[#6366F1]'}`}>
90
90
  {item.type === 'builtin' ? 'Built-in' : 'Custom'}
91
91
  </span>
92
92
  {!inSidebar && item.type === 'custom' && (
@@ -94,7 +94,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
94
94
  <div
95
95
  onClick={(e) => handleToggle(e, item.id, item.isEnabled)}
96
96
  className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
97
- ${item.isEnabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
97
+ ${item.isEnabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
98
98
  >
99
99
  <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
100
100
  ${item.isEnabled ? 'left-[18px]' : 'left-0.5'}`} />
@@ -342,7 +342,7 @@ export function ProviderSheet() {
342
342
  <div
343
343
  onClick={() => setRequiresApiKey(!requiresApiKey)}
344
344
  className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
345
- ${requiresApiKey ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
345
+ ${requiresApiKey ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
346
346
  >
347
347
  <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
348
348
  ${requiresApiKey ? 'left-[22px]' : 'left-0.5'}`} />
@@ -441,7 +441,7 @@ export function ProviderSheet() {
441
441
  <div
442
442
  onClick={() => setIsEnabled(!isEnabled)}
443
443
  className={`w-11 h-6 rounded-full transition-all duration-200 relative cursor-pointer
444
- ${isEnabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
444
+ ${isEnabled ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
445
445
  >
446
446
  <div className={`absolute top-0.5 w-5 h-5 rounded-full bg-white transition-all duration-200
447
447
  ${isEnabled ? 'left-[22px]' : 'left-0.5'}`} />
@@ -485,7 +485,7 @@ export function ProviderSheet() {
485
485
  <button
486
486
  onClick={handleSave}
487
487
  disabled={isBuiltin ? false : (!name.trim() || !baseUrl.trim())}
488
- className="flex-1 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
488
+ className="flex-1 py-3.5 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
489
489
  style={{ fontFamily: 'inherit' }}
490
490
  >
491
491
  {editing ? 'Save' : 'Create'}
@@ -3,6 +3,7 @@
3
3
  import type { Schedule } from '@/types'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
+ import { cronToHuman } from '@/lib/cron-human'
6
7
 
7
8
  const STATUS_COLORS: Record<string, string> = {
8
9
  active: 'text-emerald-400 bg-emerald-400/[0.08]',
@@ -70,7 +71,7 @@ export function ScheduleCard({ schedule, inSidebar }: Props) {
70
71
  <div
71
72
  onClick={handleToggle}
72
73
  className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
73
- ${schedule.status === 'active' ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
74
+ ${schedule.status === 'active' ? 'bg-accent-bright' : 'bg-white/[0.08]'}`}
74
75
  >
75
76
  <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
76
77
  ${schedule.status === 'active' ? 'left-[18px]' : 'left-0.5'}`} />
@@ -95,7 +96,7 @@ export function ScheduleCard({ schedule, inSidebar }: Props) {
95
96
  <div className="text-[12px] text-text-3/70 mt-1.5 truncate">
96
97
  {agent?.name || 'Unknown agent'} &middot; {schedule.scheduleType}
97
98
  {!inSidebar && schedule.scheduleType === 'cron' && schedule.cron && (
98
- <span className="font-mono text-text-3/50 ml-1">({schedule.cron})</span>
99
+ <span className="text-text-3/50 ml-1" title={schedule.cron}>({cronToHuman(schedule.cron)})</span>
99
100
  )}
100
101
  {!inSidebar && schedule.scheduleType === 'interval' && schedule.intervalMs && (
101
102
  <span className="text-text-3/50 ml-1">
@@ -41,7 +41,7 @@ export function ScheduleList({ inSidebar }: Props) {
41
41
  {!inSidebar && (
42
42
  <button
43
43
  onClick={() => setScheduleSheetOpen(true)}
44
- className="mt-3 px-8 py-3 rounded-[14px] border-none bg-[#6366F1] text-white
44
+ className="mt-3 px-8 py-3 rounded-[14px] border-none bg-accent-bright text-white
45
45
  text-[14px] font-600 cursor-pointer active:scale-95 transition-all duration-200
46
46
  shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
47
47
  style={{ fontFamily: 'inherit' }}
@@ -4,8 +4,12 @@ import { useEffect, useState, useMemo } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { createSchedule, updateSchedule, deleteSchedule } from '@/lib/schedules'
6
6
  import { BottomSheet } from '@/components/shared/bottom-sheet'
7
+ import { AgentPickerList } from '@/components/shared/agent-picker-list'
8
+ import { SheetFooter } from '@/components/shared/sheet-footer'
9
+ import { inputClass } from '@/components/shared/form-styles'
7
10
  import type { ScheduleType, ScheduleStatus } from '@/types'
8
11
  import cronstrue from 'cronstrue'
12
+ import { SectionLabel } from '@/components/shared/section-label'
9
13
 
10
14
  const CRON_PRESETS = [
11
15
  { label: 'Every hour', cron: '0 * * * *' },
@@ -62,7 +66,7 @@ export function ScheduleSheet() {
62
66
  const [customCron, setCustomCron] = useState(false)
63
67
 
64
68
  const editing = editingId ? schedules[editingId] : null
65
- const agentList = Object.values(agents)
69
+ const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
66
70
 
67
71
  useEffect(() => {
68
72
  if (open) {
@@ -125,8 +129,6 @@ export function ScheduleSheet() {
125
129
  }
126
130
  }
127
131
 
128
- const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
129
-
130
132
  return (
131
133
  <BottomSheet open={open} onClose={onClose} wide>
132
134
  <div className="mb-10">
@@ -137,22 +139,22 @@ export function ScheduleSheet() {
137
139
  </div>
138
140
 
139
141
  <div className="mb-8">
140
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
142
+ <SectionLabel>Name</SectionLabel>
141
143
  <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Daily keyword research" className={inputClass} style={{ fontFamily: 'inherit' }} />
142
144
  </div>
143
145
 
144
146
  <div className="mb-8">
145
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Agent</label>
146
- <select value={agentId || ''} onChange={(e) => setAgentId(e.target.value)} className={`${inputClass} appearance-none cursor-pointer`} style={{ fontFamily: 'inherit' }}>
147
- <option value="">Select agent...</option>
148
- {agentList.map((p) => (
149
- <option key={p.id} value={p.id}>{p.name}{p.isOrchestrator ? ' (Orchestrator)' : ''}</option>
150
- ))}
151
- </select>
147
+ <SectionLabel>Agent</SectionLabel>
148
+ <AgentPickerList
149
+ agents={agentList}
150
+ selected={agentId}
151
+ onSelect={(id) => setAgentId(id)}
152
+ showOrchBadge={true}
153
+ />
152
154
  </div>
153
155
 
154
156
  <div className="mb-8">
155
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Task Prompt</label>
157
+ <SectionLabel>Task Prompt</SectionLabel>
156
158
  <textarea
157
159
  value={taskPrompt}
158
160
  onChange={(e) => setTaskPrompt(e.target.value)}
@@ -164,7 +166,7 @@ export function ScheduleSheet() {
164
166
  </div>
165
167
 
166
168
  <div className="mb-8">
167
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Schedule Type</label>
169
+ <SectionLabel>Schedule Type</SectionLabel>
168
170
  <div className="grid grid-cols-3 gap-3">
169
171
  {(['cron', 'interval', 'once'] as ScheduleType[]).map((t) => (
170
172
  <button
@@ -185,7 +187,7 @@ export function ScheduleSheet() {
185
187
 
186
188
  {scheduleType === 'cron' && (
187
189
  <div className="mb-8">
188
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Schedule</label>
190
+ <SectionLabel>Schedule</SectionLabel>
189
191
 
190
192
  {/* Preset buttons */}
191
193
  <div className="flex flex-wrap gap-2 mb-4">
@@ -239,7 +241,7 @@ export function ScheduleSheet() {
239
241
 
240
242
  {scheduleType === 'interval' && (
241
243
  <div className="mb-8">
242
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Interval (minutes)</label>
244
+ <SectionLabel>Interval (minutes)</SectionLabel>
243
245
  <input
244
246
  type="number"
245
247
  value={Math.round(intervalMs / 60000)}
@@ -252,7 +254,7 @@ export function ScheduleSheet() {
252
254
 
253
255
  {editing && (
254
256
  <div className="mb-8">
255
- <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Status</label>
257
+ <SectionLabel>Status</SectionLabel>
256
258
  <div className="flex gap-2">
257
259
  {(['active', 'paused'] as ScheduleStatus[]).map((s) => (
258
260
  <button
@@ -271,19 +273,17 @@ export function ScheduleSheet() {
271
273
  </div>
272
274
  )}
273
275
 
274
- <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
275
- {editing && (
276
+ <SheetFooter
277
+ onCancel={onClose}
278
+ onSave={handleSave}
279
+ saveLabel={editing ? 'Save' : 'Create'}
280
+ saveDisabled={!name.trim() || !agentId}
281
+ left={editing && (
276
282
  <button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
277
283
  Delete
278
284
  </button>
279
285
  )}
280
- <button onClick={onClose} className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" style={{ fontFamily: 'inherit' }}>
281
- Cancel
282
- </button>
283
- <button onClick={handleSave} disabled={!name.trim() || !agentId} className="flex-1 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110" style={{ fontFamily: 'inherit' }}>
284
- {editing ? 'Save' : 'Create'}
285
- </button>
286
- </div>
286
+ />
287
287
  </BottomSheet>
288
288
  )
289
289
  }