@swarmclawai/swarmclaw 0.3.1 → 0.4.5

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 (203) hide show
  1. package/README.md +33 -13
  2. package/bin/server-cmd.js +14 -7
  3. package/bin/swarmclaw.js +3 -1
  4. package/bin/update-cmd.js +120 -0
  5. package/next.config.ts +10 -0
  6. package/package.json +4 -1
  7. package/src/app/api/agents/[id]/route.ts +20 -18
  8. package/src/app/api/agents/[id]/thread/route.ts +4 -3
  9. package/src/app/api/agents/route.ts +8 -3
  10. package/src/app/api/auth/route.ts +3 -1
  11. package/src/app/api/claude-skills/route.ts +3 -1
  12. package/src/app/api/clawhub/install/route.ts +2 -2
  13. package/src/app/api/connectors/[id]/route.ts +14 -3
  14. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  15. package/src/app/api/connectors/route.ts +12 -4
  16. package/src/app/api/credentials/[id]/route.ts +2 -1
  17. package/src/app/api/credentials/route.ts +5 -3
  18. package/src/app/api/daemon/route.ts +6 -1
  19. package/src/app/api/documents/route.ts +2 -2
  20. package/src/app/api/files/serve/route.ts +8 -0
  21. package/src/app/api/ip/route.ts +3 -1
  22. package/src/app/api/knowledge/[id]/route.ts +5 -4
  23. package/src/app/api/knowledge/upload/route.ts +2 -2
  24. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  25. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  26. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  27. package/src/app/api/mcp-servers/route.ts +5 -3
  28. package/src/app/api/memory/[id]/route.ts +9 -8
  29. package/src/app/api/memory/route.ts +2 -2
  30. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  31. package/src/app/api/openclaw/directory/route.ts +26 -0
  32. package/src/app/api/openclaw/discover/route.ts +61 -0
  33. package/src/app/api/openclaw/sync/route.ts +30 -0
  34. package/src/app/api/orchestrator/graph/route.ts +25 -0
  35. package/src/app/api/orchestrator/run/route.ts +2 -2
  36. package/src/app/api/plugins/marketplace/route.ts +3 -1
  37. package/src/app/api/plugins/route.ts +3 -1
  38. package/src/app/api/projects/[id]/route.ts +55 -0
  39. package/src/app/api/projects/route.ts +27 -0
  40. package/src/app/api/providers/[id]/models/route.ts +2 -1
  41. package/src/app/api/providers/[id]/route.ts +13 -12
  42. package/src/app/api/providers/configs/route.ts +3 -1
  43. package/src/app/api/providers/route.ts +7 -3
  44. package/src/app/api/schedules/[id]/route.ts +16 -15
  45. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  46. package/src/app/api/schedules/route.ts +8 -3
  47. package/src/app/api/secrets/[id]/route.ts +16 -17
  48. package/src/app/api/secrets/route.ts +5 -3
  49. package/src/app/api/sessions/[id]/chat/route.ts +5 -2
  50. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  51. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  52. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  53. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  54. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  55. package/src/app/api/sessions/[id]/route.ts +2 -1
  56. package/src/app/api/sessions/route.ts +11 -4
  57. package/src/app/api/settings/route.ts +3 -1
  58. package/src/app/api/setup/doctor/route.ts +1 -0
  59. package/src/app/api/setup/openclaw-device/route.ts +3 -1
  60. package/src/app/api/skills/[id]/route.ts +23 -21
  61. package/src/app/api/skills/import/route.ts +2 -2
  62. package/src/app/api/skills/route.ts +5 -3
  63. package/src/app/api/tasks/[id]/approve/route.ts +74 -0
  64. package/src/app/api/tasks/[id]/route.ts +9 -5
  65. package/src/app/api/tasks/route.ts +5 -2
  66. package/src/app/api/tts/stream/route.ts +48 -0
  67. package/src/app/api/upload/route.ts +2 -2
  68. package/src/app/api/uploads/[filename]/route.ts +4 -1
  69. package/src/app/api/usage/route.ts +3 -1
  70. package/src/app/api/version/route.ts +3 -1
  71. package/src/app/api/webhooks/[id]/route.ts +31 -32
  72. package/src/app/api/webhooks/route.ts +5 -3
  73. package/src/app/icon.svg +58 -0
  74. package/src/app/page.tsx +11 -26
  75. package/src/cli/index.js +28 -9
  76. package/src/cli/index.ts +45 -2
  77. package/src/cli/spec.js +2 -8
  78. package/src/components/agents/agent-card.tsx +1 -1
  79. package/src/components/agents/agent-list.tsx +3 -1
  80. package/src/components/agents/agent-sheet.tsx +166 -81
  81. package/src/components/chat/chat-area.tsx +71 -34
  82. package/src/components/chat/chat-header.tsx +141 -29
  83. package/src/components/chat/chat-tool-toggles.tsx +12 -53
  84. package/src/components/chat/message-bubble.tsx +110 -42
  85. package/src/components/chat/tool-call-bubble.tsx +50 -6
  86. package/src/components/chat/tool-request-banner.tsx +1 -9
  87. package/src/components/chat/voice-overlay.tsx +80 -0
  88. package/src/components/connectors/connector-list.tsx +9 -10
  89. package/src/components/connectors/connector-sheet.tsx +55 -36
  90. package/src/components/input/chat-input.tsx +72 -56
  91. package/src/components/knowledge/knowledge-list.tsx +27 -31
  92. package/src/components/layout/app-layout.tsx +133 -90
  93. package/src/components/layout/daemon-indicator.tsx +3 -5
  94. package/src/components/logs/log-list.tsx +5 -9
  95. package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
  96. package/src/components/memory/memory-detail.tsx +1 -1
  97. package/src/components/plugins/plugin-list.tsx +227 -27
  98. package/src/components/projects/project-list.tsx +122 -0
  99. package/src/components/projects/project-sheet.tsx +135 -0
  100. package/src/components/providers/provider-list.tsx +46 -13
  101. package/src/components/providers/provider-sheet.tsx +0 -45
  102. package/src/components/runs/run-list.tsx +6 -15
  103. package/src/components/schedules/schedule-card.tsx +54 -4
  104. package/src/components/schedules/schedule-list.tsx +9 -4
  105. package/src/components/schedules/schedule-sheet.tsx +0 -47
  106. package/src/components/secrets/secrets-list.tsx +20 -2
  107. package/src/components/sessions/new-session-sheet.tsx +14 -15
  108. package/src/components/sessions/session-card.tsx +1 -1
  109. package/src/components/sessions/session-list.tsx +7 -7
  110. package/src/components/shared/connector-platform-icon.tsx +26 -20
  111. package/src/components/shared/model-combobox.tsx +148 -0
  112. package/src/components/shared/settings/section-heartbeat.tsx +8 -40
  113. package/src/components/shared/settings/section-orchestrator.tsx +9 -11
  114. package/src/components/shared/settings/section-web-search.tsx +56 -0
  115. package/src/components/shared/settings/settings-page.tsx +73 -0
  116. package/src/components/skills/skill-list.tsx +262 -35
  117. package/src/components/skills/skill-sheet.tsx +0 -45
  118. package/src/components/tasks/task-board.tsx +3 -6
  119. package/src/components/tasks/task-card.tsx +43 -1
  120. package/src/components/tasks/task-list.tsx +8 -7
  121. package/src/components/tasks/task-sheet.tsx +0 -44
  122. package/src/components/usage/usage-list.tsx +12 -4
  123. package/src/hooks/use-continuous-speech.ts +144 -0
  124. package/src/hooks/use-view-router.ts +52 -0
  125. package/src/hooks/use-voice-conversation.ts +80 -0
  126. package/src/hooks/use-ws.ts +66 -0
  127. package/src/instrumentation.ts +2 -0
  128. package/src/lib/chat.ts +14 -2
  129. package/src/lib/id.ts +6 -0
  130. package/src/lib/projects.ts +13 -0
  131. package/src/lib/provider-sets.ts +5 -0
  132. package/src/lib/providers/anthropic.ts +15 -2
  133. package/src/lib/providers/index.ts +8 -0
  134. package/src/lib/providers/ollama.ts +10 -2
  135. package/src/lib/providers/openai.ts +42 -13
  136. package/src/lib/providers/openclaw.ts +11 -0
  137. package/src/lib/server/api-routes.test.ts +5 -6
  138. package/src/lib/server/build-llm.ts +17 -4
  139. package/src/lib/server/chat-execution.ts +57 -8
  140. package/src/lib/server/collection-helpers.ts +54 -0
  141. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  142. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  143. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  144. package/src/lib/server/connectors/googlechat.ts +46 -7
  145. package/src/lib/server/connectors/manager.ts +401 -6
  146. package/src/lib/server/connectors/media.ts +2 -2
  147. package/src/lib/server/connectors/openclaw.ts +64 -0
  148. package/src/lib/server/connectors/pairing.test.ts +99 -0
  149. package/src/lib/server/connectors/pairing.ts +256 -0
  150. package/src/lib/server/connectors/signal.ts +1 -0
  151. package/src/lib/server/connectors/teams.ts +5 -5
  152. package/src/lib/server/connectors/types.ts +10 -0
  153. package/src/lib/server/context-manager.ts +1 -1
  154. package/src/lib/server/daemon-state.ts +3 -0
  155. package/src/lib/server/data-dir.ts +1 -0
  156. package/src/lib/server/execution-log.ts +3 -3
  157. package/src/lib/server/heartbeat-service.ts +67 -3
  158. package/src/lib/server/knowledge-db.test.ts +2 -33
  159. package/src/lib/server/langgraph-checkpoint.ts +274 -0
  160. package/src/lib/server/main-agent-loop.ts +67 -8
  161. package/src/lib/server/memory-db.ts +6 -6
  162. package/src/lib/server/openclaw-approvals.ts +105 -0
  163. package/src/lib/server/openclaw-sync.ts +496 -0
  164. package/src/lib/server/orchestrator-lg.ts +422 -20
  165. package/src/lib/server/orchestrator.ts +29 -9
  166. package/src/lib/server/process-manager.ts +2 -2
  167. package/src/lib/server/queue.ts +39 -13
  168. package/src/lib/server/scheduler.ts +2 -2
  169. package/src/lib/server/session-mailbox.ts +2 -2
  170. package/src/lib/server/session-run-manager.ts +8 -3
  171. package/src/lib/server/session-tools/connector.ts +51 -4
  172. package/src/lib/server/session-tools/crud.ts +3 -3
  173. package/src/lib/server/session-tools/delegate.ts +5 -5
  174. package/src/lib/server/session-tools/file.ts +176 -3
  175. package/src/lib/server/session-tools/index.ts +4 -0
  176. package/src/lib/server/session-tools/memory.ts +2 -2
  177. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  178. package/src/lib/server/session-tools/sandbox.ts +197 -0
  179. package/src/lib/server/session-tools/search-providers.ts +270 -0
  180. package/src/lib/server/session-tools/session-info.ts +2 -2
  181. package/src/lib/server/session-tools/web.ts +47 -66
  182. package/src/lib/server/storage-mcp.test.ts +25 -2
  183. package/src/lib/server/storage.ts +36 -7
  184. package/src/lib/server/stream-agent-chat.ts +106 -22
  185. package/src/lib/server/task-result.test.ts +44 -0
  186. package/src/lib/server/task-result.ts +14 -0
  187. package/src/lib/server/task-validation.test.ts +23 -0
  188. package/src/lib/server/task-validation.ts +5 -3
  189. package/src/lib/server/ws-hub.ts +85 -0
  190. package/src/lib/tool-definitions.ts +44 -0
  191. package/src/lib/tts-stream.ts +130 -0
  192. package/src/lib/upload.ts +7 -1
  193. package/src/lib/view-routes.ts +28 -0
  194. package/src/lib/ws-client.ts +124 -0
  195. package/src/proxy.ts +3 -0
  196. package/src/stores/use-app-store.ts +28 -1
  197. package/src/stores/use-chat-store.ts +42 -14
  198. package/src/types/index.ts +34 -2
  199. package/src/app/api/agents/generate/route.ts +0 -42
  200. package/src/app/api/generate/info/route.ts +0 -12
  201. package/src/app/api/generate/route.ts +0 -106
  202. package/src/app/favicon.ico +0 -0
  203. package/src/components/shared/ai-gen-block.tsx +0 -77
@@ -59,8 +59,19 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
59
59
  await loadMcpServers()
60
60
  }
61
61
 
62
+ const handleRetest = async (e: React.MouseEvent, id: string) => {
63
+ e.stopPropagation()
64
+ setStatuses((prev) => ({ ...prev, [id]: { ok: false, loading: true } }))
65
+ try {
66
+ const res = await api<{ ok: boolean; tools?: string[]; error?: string }>('POST', `/mcp-servers/${id}/test`)
67
+ setStatuses((prev) => ({ ...prev, [id]: { ok: res.ok, tools: res.tools, error: res.error, loading: false } }))
68
+ } catch {
69
+ setStatuses((prev) => ({ ...prev, [id]: { ok: false, error: 'Test failed', loading: false } }))
70
+ }
71
+ }
72
+
62
73
  return (
63
- <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-4'}`}>
74
+ <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
64
75
  {serverList.length === 0 ? (
65
76
  <div className="text-center py-12">
66
77
  <p className="text-[13px] text-text-3/60">No MCP servers configured</p>
@@ -73,7 +84,7 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
73
84
  </button>
74
85
  </div>
75
86
  ) : (
76
- <div className="space-y-2">
87
+ <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
77
88
  {serverList.map((server) => (
78
89
  <button
79
90
  key={server.id}
@@ -96,6 +107,17 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
96
107
  <span className="font-display text-[14px] font-600 text-text truncate">{server.name}</span>
97
108
  </div>
98
109
  <div className="flex items-center gap-2 shrink-0 ml-2">
110
+ {!inSidebar && (
111
+ <button
112
+ onClick={(e) => handleRetest(e, server.id)}
113
+ className="text-text-3/40 hover:text-text-2 transition-colors p-0.5"
114
+ title="Re-test connection"
115
+ >
116
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
117
+ <path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16" />
118
+ </svg>
119
+ </button>
120
+ )}
99
121
  <span className={`text-[10px] font-mono px-2 py-0.5 rounded-full ${transportColors[server.transport] || 'bg-white/10 text-text-3'}`}>
100
122
  {server.transport}
101
123
  </span>
@@ -76,7 +76,7 @@ export function MemoryDetail() {
76
76
 
77
77
  const handleNavigateToSession = useCallback(() => {
78
78
  if (!entry?.sessionId) return
79
- setActiveView('sessions')
79
+ setActiveView('agents')
80
80
  setCurrentSession(entry.sessionId)
81
81
  }, [entry])
82
82
 
@@ -1,7 +1,9 @@
1
1
  'use client'
2
2
 
3
- import { useEffect } from 'react'
3
+ import { useEffect, useState, useCallback } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { api } from '@/lib/api-client'
6
+ import type { MarketplacePlugin } from '@/types'
5
7
 
6
8
  export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
7
9
  const plugins = useAppStore((s) => s.plugins)
@@ -9,10 +11,31 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
9
11
  const setPluginSheetOpen = useAppStore((s) => s.setPluginSheetOpen)
10
12
  const setEditingPluginFilename = useAppStore((s) => s.setEditingPluginFilename)
11
13
 
14
+ const [tab, setTab] = useState<'installed' | 'marketplace'>('installed')
15
+ const [marketplace, setMarketplace] = useState<MarketplacePlugin[]>([])
16
+ const [mpLoading, setMpLoading] = useState(false)
17
+ const [installing, setInstalling] = useState<string | null>(null)
18
+ const [search, setSearch] = useState('')
19
+ const [activeTag, setActiveTag] = useState<string | null>(null)
20
+ const [sort, setSort] = useState<'name' | 'downloads'>('downloads')
21
+
12
22
  useEffect(() => {
13
23
  loadPlugins()
14
24
  }, [])
15
25
 
26
+ const loadMarketplace = useCallback(async () => {
27
+ setMpLoading(true)
28
+ try {
29
+ const data = await api<MarketplacePlugin[]>('GET', '/plugins/marketplace')
30
+ if (Array.isArray(data)) setMarketplace(data)
31
+ } catch { /* ignore */ }
32
+ setMpLoading(false)
33
+ }, [])
34
+
35
+ useEffect(() => {
36
+ if (!inSidebar && tab === 'marketplace') loadMarketplace()
37
+ }, [tab, inSidebar, loadMarketplace])
38
+
16
39
  const pluginList = Object.values(plugins)
17
40
 
18
41
  const handleEdit = (filename: string) => {
@@ -20,41 +43,218 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
20
43
  setPluginSheetOpen(true)
21
44
  }
22
45
 
23
- return (
24
- <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-4'}`}>
25
- {pluginList.length === 0 ? (
26
- <div className="text-center py-12">
27
- <p className="text-[13px] text-text-3/60">No plugins installed</p>
46
+ const handleToggle = async (e: React.MouseEvent, filename: string, enabled: boolean) => {
47
+ e.stopPropagation()
48
+ await api('POST', '/plugins', { filename, enabled: !enabled })
49
+ loadPlugins()
50
+ }
51
+
52
+ const handleDelete = async (e: React.MouseEvent, filename: string) => {
53
+ e.stopPropagation()
54
+ await api('DELETE', `/plugins/${encodeURIComponent(filename)}`)
55
+ loadPlugins()
56
+ }
57
+
58
+ const installFromMarketplace = async (p: MarketplacePlugin) => {
59
+ setInstalling(p.id)
60
+ try {
61
+ await api('POST', '/plugins/install', { url: p.url, filename: `${p.id}.js` })
62
+ await loadPlugins()
63
+ } catch { /* ignore */ }
64
+ setInstalling(null)
65
+ }
66
+
67
+ const installedFilenames = new Set(Object.keys(plugins))
68
+
69
+ const tabClass = (t: string) =>
70
+ `py-1.5 px-3.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
71
+ ${tab === t
72
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
73
+ : 'bg-transparent border-transparent text-text-3 hover:text-text-2'}`
74
+
75
+ // Marketplace tab content (full-width only)
76
+ const renderMarketplace = () => {
77
+ if (mpLoading) return <p className="text-[12px] text-text-3/70 py-8 text-center">Loading marketplace...</p>
78
+ if (marketplace.length === 0) return <p className="text-[12px] text-text-3/70 py-8 text-center">No plugins available</p>
79
+
80
+ const allTags = Array.from(new Set(marketplace.flatMap((p) => p.tags))).sort()
81
+ const q = search.toLowerCase()
82
+ const filtered = marketplace
83
+ .filter((p) => {
84
+ if (q && !p.name.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q) && !p.tags.some((t) => t.toLowerCase().includes(q))) return false
85
+ if (activeTag && !p.tags.includes(activeTag)) return false
86
+ return true
87
+ })
88
+ .sort((a, b) => sort === 'downloads' ? b.downloads - a.downloads : a.name.localeCompare(b.name))
89
+
90
+ return (
91
+ <div className="space-y-3">
92
+ <input
93
+ value={search}
94
+ onChange={(e) => setSearch(e.target.value)}
95
+ placeholder="Search plugins..."
96
+ className="w-full px-3 py-2.5 rounded-[10px] bg-surface border border-white/[0.06] text-[12px] text-text placeholder:text-text-3/50 outline-none focus:border-accent-bright/30"
97
+ style={{ fontFamily: 'inherit' }}
98
+ />
99
+ <div className="flex items-center gap-1.5 flex-wrap">
28
100
  <button
29
- onClick={() => { setEditingPluginFilename(null); setPluginSheetOpen(true) }}
30
- className="mt-3 px-4 py-2 rounded-[10px] bg-transparent text-accent-bright text-[13px] font-600 cursor-pointer border border-accent-bright/20 hover:bg-accent-soft transition-all"
31
- style={{ fontFamily: 'inherit' }}
101
+ onClick={() => setActiveTag(null)}
102
+ className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
103
+ !activeTag ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
104
+ }`}
32
105
  >
33
- + Add Plugin
106
+ All
34
107
  </button>
35
- </div>
36
- ) : (
37
- <div className="space-y-2">
38
- {pluginList.map((plugin) => (
108
+ {allTags.map((t) => (
39
109
  <button
40
- key={plugin.filename}
41
- onClick={() => handleEdit(plugin.filename)}
42
- className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
110
+ key={t}
111
+ onClick={() => setActiveTag(activeTag === t ? null : t)}
112
+ className={`px-2 py-1 rounded-[6px] text-[10px] font-600 cursor-pointer transition-all border-none ${
113
+ activeTag === t ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.03] text-text-3/60 hover:text-text-3'
114
+ }`}
43
115
  >
44
- <div className="flex items-center justify-between mb-1">
45
- <span className="font-display text-[14px] font-600 text-text truncate">{plugin.name}</span>
46
- <span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-full shrink-0 ml-2 ${plugin.enabled ? 'text-emerald-400 bg-emerald-400/10' : 'text-text-3/50 bg-white/[0.04]'}`}>
47
- {plugin.enabled ? 'Enabled' : 'Disabled'}
48
- </span>
49
- </div>
50
- <div className="text-[11px] font-mono text-text-3/50 mb-1">{plugin.filename}</div>
51
- {plugin.description && (
52
- <p className="text-[12px] text-text-3/60 line-clamp-2">{plugin.description}</p>
53
- )}
116
+ {t}
54
117
  </button>
55
118
  ))}
119
+ <div className="flex-1" />
120
+ <select
121
+ value={sort}
122
+ onChange={(e) => setSort(e.target.value as 'name' | 'downloads')}
123
+ className="px-2 py-1 rounded-[6px] bg-surface border border-white/[0.06] text-[10px] text-text-3 outline-none cursor-pointer appearance-none"
124
+ style={{ fontFamily: 'inherit' }}
125
+ >
126
+ <option value="downloads">Popular</option>
127
+ <option value="name">A-Z</option>
128
+ </select>
129
+ </div>
130
+ {filtered.length === 0 ? (
131
+ <p className="text-[12px] text-text-3/50 text-center py-4">No plugins match your search</p>
132
+ ) : (
133
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
134
+ {filtered.map((p) => {
135
+ const isInstalled = installedFilenames.has(`${p.id}.js`)
136
+ return (
137
+ <div key={p.id} className="py-3.5 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
138
+ <div className="flex items-start gap-3">
139
+ <div className="flex-1 min-w-0">
140
+ <div className="flex items-center gap-2">
141
+ <span className="text-[14px] font-600 text-text">{p.name}</span>
142
+ <span className="text-[10px] font-mono text-text-3/70">v{p.version}</span>
143
+ {p.openclaw && <span className="text-[9px] font-600 text-emerald-400 bg-emerald-400/10 px-1.5 py-0.5 rounded-full">OpenClaw</span>}
144
+ </div>
145
+ <div className="text-[11px] text-text-3/60 mt-1 line-clamp-2">{p.description}</div>
146
+ <div className="flex items-center gap-2 mt-2">
147
+ <span className="text-[10px] text-text-3/70">by {p.author}</span>
148
+ <span className="text-[10px] text-text-3/50">&middot;</span>
149
+ {p.tags.slice(0, 3).map((t) => (
150
+ <button
151
+ key={t}
152
+ onClick={() => setActiveTag(activeTag === t ? null : t)}
153
+ className={`text-[9px] font-600 px-1.5 py-0.5 rounded-full cursor-pointer transition-all border-none ${
154
+ activeTag === t ? 'text-accent-bright bg-accent-soft' : 'text-text-3/50 bg-white/[0.04] hover:text-text-3'
155
+ }`}
156
+ >
157
+ {t}
158
+ </button>
159
+ ))}
160
+ </div>
161
+ </div>
162
+ <button
163
+ onClick={() => !isInstalled && installFromMarketplace(p)}
164
+ disabled={isInstalled || installing === p.id}
165
+ className={`shrink-0 py-2 px-4 rounded-[10px] text-[12px] font-600 transition-all cursor-pointer
166
+ ${isInstalled
167
+ ? 'bg-white/[0.04] text-text-3/70 cursor-default'
168
+ : installing === p.id
169
+ ? 'bg-accent-soft text-accent-bright animate-pulse'
170
+ : 'bg-accent-soft text-accent-bright hover:bg-accent-soft/80 border border-accent-bright/20'}`}
171
+ style={{ fontFamily: 'inherit' }}
172
+ >
173
+ {isInstalled ? 'Installed' : installing === p.id ? 'Installing...' : 'Install'}
174
+ </button>
175
+ </div>
176
+ </div>
177
+ )
178
+ })}
179
+ </div>
180
+ )}
181
+ </div>
182
+ )
183
+ }
184
+
185
+ return (
186
+ <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
187
+ {/* Tabs — full-width only */}
188
+ {!inSidebar && (
189
+ <div className="flex gap-1 mb-4">
190
+ <button onClick={() => setTab('installed')} className={tabClass('installed')} style={{ fontFamily: 'inherit' }}>
191
+ Installed
192
+ </button>
193
+ <button onClick={() => setTab('marketplace')} className={tabClass('marketplace')} style={{ fontFamily: 'inherit' }}>
194
+ Marketplace
195
+ </button>
56
196
  </div>
57
197
  )}
198
+
199
+ {(!inSidebar && tab === 'marketplace') ? renderMarketplace() : (
200
+ pluginList.length === 0 ? (
201
+ <div className="text-center py-12">
202
+ <p className="text-[13px] text-text-3/60">No plugins installed</p>
203
+ <button
204
+ onClick={() => { setEditingPluginFilename(null); setPluginSheetOpen(true) }}
205
+ className="mt-3 px-4 py-2 rounded-[10px] bg-transparent text-accent-bright text-[13px] font-600 cursor-pointer border border-accent-bright/20 hover:bg-accent-soft transition-all"
206
+ style={{ fontFamily: 'inherit' }}
207
+ >
208
+ + Add Plugin
209
+ </button>
210
+ </div>
211
+ ) : (
212
+ <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
213
+ {pluginList.map((plugin) => (
214
+ <button
215
+ key={plugin.filename}
216
+ onClick={() => handleEdit(plugin.filename)}
217
+ className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
218
+ >
219
+ <div className="flex items-center justify-between mb-1">
220
+ <span className="font-display text-[14px] font-600 text-text truncate">{plugin.name}</span>
221
+ <div className="flex items-center gap-2 shrink-0 ml-2">
222
+ {!inSidebar ? (
223
+ <>
224
+ <div
225
+ onClick={(e) => handleToggle(e, plugin.filename, plugin.enabled)}
226
+ className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
227
+ ${plugin.enabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
228
+ >
229
+ <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
230
+ ${plugin.enabled ? 'left-[18px]' : 'left-0.5'}`} />
231
+ </div>
232
+ <button
233
+ onClick={(e) => handleDelete(e, plugin.filename)}
234
+ className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
235
+ title="Delete"
236
+ >
237
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
238
+ <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
239
+ </svg>
240
+ </button>
241
+ </>
242
+ ) : (
243
+ <span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-full ${plugin.enabled ? 'text-emerald-400 bg-emerald-400/10' : 'text-text-3/50 bg-white/[0.04]'}`}>
244
+ {plugin.enabled ? 'Enabled' : 'Disabled'}
245
+ </span>
246
+ )}
247
+ </div>
248
+ </div>
249
+ <div className="text-[11px] font-mono text-text-3/50 mb-1">{plugin.filename}</div>
250
+ {plugin.description && (
251
+ <p className="text-[12px] text-text-3/60 line-clamp-2">{plugin.description}</p>
252
+ )}
253
+ </button>
254
+ ))}
255
+ </div>
256
+ )
257
+ )}
58
258
  </div>
59
259
  )
60
260
  }
@@ -0,0 +1,122 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useMemo, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+
6
+ export function ProjectList() {
7
+ const projects = useAppStore((s) => s.projects)
8
+ const loadProjects = useAppStore((s) => s.loadProjects)
9
+ const agents = useAppStore((s) => s.agents)
10
+ const tasks = useAppStore((s) => s.tasks)
11
+ const setProjectSheetOpen = useAppStore((s) => s.setProjectSheetOpen)
12
+ const setEditingProjectId = useAppStore((s) => s.setEditingProjectId)
13
+ const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
14
+ const setActiveProjectFilter = useAppStore((s) => s.setActiveProjectFilter)
15
+ const [search, setSearch] = useState('')
16
+
17
+ useEffect(() => { loadProjects() }, [])
18
+
19
+ const filtered = useMemo(() => {
20
+ return Object.values(projects)
21
+ .filter((p) => {
22
+ if (search && !p.name.toLowerCase().includes(search.toLowerCase())) return false
23
+ return true
24
+ })
25
+ .sort((a, b) => b.updatedAt - a.updatedAt)
26
+ }, [projects, search])
27
+
28
+ const entityCounts = useMemo(() => {
29
+ const counts: Record<string, { agents: number; tasks: number }> = {}
30
+ for (const p of Object.values(projects)) {
31
+ counts[p.id] = { agents: 0, tasks: 0 }
32
+ }
33
+ for (const a of Object.values(agents)) {
34
+ if (a.projectId && counts[a.projectId]) counts[a.projectId].agents++
35
+ }
36
+ for (const t of Object.values(tasks)) {
37
+ if (t.projectId && counts[t.projectId]) counts[t.projectId].tasks++
38
+ }
39
+ return counts
40
+ }, [projects, agents, tasks])
41
+
42
+ if (!filtered.length && !search) {
43
+ return (
44
+ <div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
45
+ <div className="w-12 h-12 rounded-[14px] bg-accent-soft flex items-center justify-center mb-1">
46
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright">
47
+ <path d="M2 20a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8l-7-7H4a2 2 0 0 0-2 2v17Z" />
48
+ <path d="M14 2v7h7" />
49
+ </svg>
50
+ </div>
51
+ <p className="font-display text-[15px] font-600 text-text-2">No projects yet</p>
52
+ <p className="text-[13px] text-text-3/50">Group agents, tasks, and schedules into projects</p>
53
+ <button
54
+ onClick={() => { setEditingProjectId(null); setProjectSheetOpen(true) }}
55
+ 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"
56
+ >
57
+ <span className="text-lg leading-none">+</span> New Project
58
+ </button>
59
+ </div>
60
+ )
61
+ }
62
+
63
+ return (
64
+ <div className="flex-1 flex flex-col h-full overflow-y-auto">
65
+ <div className="p-4 pb-0">
66
+ <div className="flex items-center gap-2 mb-4">
67
+ <input
68
+ type="text"
69
+ value={search}
70
+ onChange={(e) => setSearch(e.target.value)}
71
+ placeholder="Search projects..."
72
+ 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"
73
+ style={{ fontFamily: 'inherit' }}
74
+ />
75
+ </div>
76
+ </div>
77
+ <div className="flex-1 overflow-y-auto px-4 pb-4 space-y-2">
78
+ {filtered.map((project) => {
79
+ const counts = entityCounts[project.id] || { agents: 0, tasks: 0 }
80
+ const isActive = activeProjectFilter === project.id
81
+ return (
82
+ <div
83
+ key={project.id}
84
+ className={`group relative p-4 rounded-xl border transition-colors cursor-pointer ${
85
+ isActive
86
+ ? 'bg-accent/10 border-accent/30'
87
+ : 'bg-white/[0.03] border-white/[0.06] hover:bg-white/[0.06]'
88
+ }`}
89
+ onClick={() => setActiveProjectFilter(isActive ? null : project.id)}
90
+ >
91
+ <div className="flex items-start justify-between gap-3">
92
+ <div className="flex items-center gap-2.5 min-w-0">
93
+ {project.color && (
94
+ <div className="w-3 h-3 rounded-full shrink-0" style={{ backgroundColor: project.color }} />
95
+ )}
96
+ <div className="min-w-0">
97
+ <div className="font-display text-[14px] font-600 text-text-1 truncate">{project.name}</div>
98
+ {project.description && (
99
+ <p className="text-[12px] text-text-3/60 mt-0.5 line-clamp-2">{project.description}</p>
100
+ )}
101
+ </div>
102
+ </div>
103
+ <button
104
+ onClick={(e) => { e.stopPropagation(); setEditingProjectId(project.id); setProjectSheetOpen(true) }}
105
+ 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"
106
+ >
107
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
108
+ <path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" />
109
+ </svg>
110
+ </button>
111
+ </div>
112
+ <div className="flex items-center gap-3 mt-2.5 text-[11px] text-text-3/50">
113
+ <span>{counts.agents} agent{counts.agents !== 1 ? 's' : ''}</span>
114
+ <span>{counts.tasks} task{counts.tasks !== 1 ? 's' : ''}</span>
115
+ </div>
116
+ </div>
117
+ )
118
+ })}
119
+ </div>
120
+ </div>
121
+ )
122
+ }
@@ -0,0 +1,135 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { createProject, updateProject, deleteProject } from '@/lib/projects'
6
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
7
+ import { toast } from 'sonner'
8
+
9
+ const PROJECT_COLORS = [
10
+ '#EF4444', '#F97316', '#EAB308', '#22C55E', '#06B6D4',
11
+ '#3B82F6', '#8B5CF6', '#EC4899', '#6B7280',
12
+ ]
13
+
14
+ const inputClass = 'w-full px-3 py-2.5 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 transition-colors'
15
+
16
+ export function ProjectSheet() {
17
+ const open = useAppStore((s) => s.projectSheetOpen)
18
+ const setOpen = useAppStore((s) => s.setProjectSheetOpen)
19
+ const editingId = useAppStore((s) => s.editingProjectId)
20
+ const setEditingId = useAppStore((s) => s.setEditingProjectId)
21
+ const projects = useAppStore((s) => s.projects)
22
+ const loadProjects = useAppStore((s) => s.loadProjects)
23
+
24
+ const [name, setName] = useState('')
25
+ const [description, setDescription] = useState('')
26
+ const [color, setColor] = useState<string | undefined>(undefined)
27
+
28
+ const editing = editingId ? projects[editingId] : null
29
+
30
+ useEffect(() => {
31
+ if (open) {
32
+ if (editing) {
33
+ setName(editing.name)
34
+ setDescription(editing.description)
35
+ setColor(editing.color)
36
+ } else {
37
+ setName('')
38
+ setDescription('')
39
+ setColor(PROJECT_COLORS[0])
40
+ }
41
+ }
42
+ // eslint-disable-next-line react-hooks/exhaustive-deps
43
+ }, [open, editingId])
44
+
45
+ const onClose = () => {
46
+ setOpen(false)
47
+ setEditingId(null)
48
+ }
49
+
50
+ const handleSave = async () => {
51
+ const data = {
52
+ name: name.trim() || 'Unnamed Project',
53
+ description,
54
+ color,
55
+ }
56
+ if (editing) {
57
+ await updateProject(editing.id, data)
58
+ } else {
59
+ await createProject(data)
60
+ }
61
+ await loadProjects()
62
+ onClose()
63
+ }
64
+
65
+ const handleDelete = async () => {
66
+ if (editing) {
67
+ await deleteProject(editing.id)
68
+ await loadProjects()
69
+ onClose()
70
+ toast.success('Project deleted')
71
+ }
72
+ }
73
+
74
+ return (
75
+ <BottomSheet open={open} onClose={onClose}>
76
+ <h2 className="font-display text-[18px] font-700 text-text mb-6">{editing ? 'Edit Project' : 'New Project'}</h2>
77
+ <div className="mb-6">
78
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Name</label>
79
+ <input
80
+ type="text"
81
+ value={name}
82
+ onChange={(e) => setName(e.target.value)}
83
+ placeholder="e.g. Marketing Site"
84
+ className={inputClass}
85
+ style={{ fontFamily: 'inherit' }}
86
+ autoFocus
87
+ />
88
+ </div>
89
+
90
+ <div className="mb-6">
91
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Description</label>
92
+ <textarea
93
+ value={description}
94
+ onChange={(e) => setDescription(e.target.value)}
95
+ placeholder="What is this project about?"
96
+ className={inputClass + ' min-h-[80px] resize-y'}
97
+ style={{ fontFamily: 'inherit' }}
98
+ rows={3}
99
+ />
100
+ </div>
101
+
102
+ <div className="mb-8">
103
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Color</label>
104
+ <div className="flex items-center gap-2">
105
+ {PROJECT_COLORS.map((c) => (
106
+ <button
107
+ key={c}
108
+ type="button"
109
+ onClick={() => setColor(c)}
110
+ className={`w-7 h-7 rounded-full transition-all ${color === c ? 'ring-2 ring-offset-2 ring-offset-surface ring-accent scale-110' : 'hover:scale-105'}`}
111
+ style={{ backgroundColor: c }}
112
+ />
113
+ ))}
114
+ </div>
115
+ </div>
116
+
117
+ <div className="flex items-center gap-3">
118
+ <button
119
+ onClick={handleSave}
120
+ className="flex-1 py-2.5 rounded-lg bg-accent text-white text-[13px] font-600 hover:bg-accent-bright transition-colors"
121
+ >
122
+ {editing ? 'Update' : 'Create'} Project
123
+ </button>
124
+ {editing && (
125
+ <button
126
+ onClick={handleDelete}
127
+ className="px-4 py-2.5 rounded-lg bg-red-500/10 text-red-400 text-[13px] font-600 hover:bg-red-500/20 transition-colors"
128
+ >
129
+ Delete
130
+ </button>
131
+ )}
132
+ </div>
133
+ </BottomSheet>
134
+ )
135
+ }