@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
@@ -1,70 +1,297 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState } 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 { Badge } from '@/components/ui/badge'
5
7
  import { ClawHubBrowser } from './clawhub-browser'
8
+ import { toast } from 'sonner'
9
+
10
+ interface ClawHubSkill {
11
+ id: string
12
+ name: string
13
+ description: string
14
+ author: string
15
+ tags: string[]
16
+ downloads: number
17
+ url: string
18
+ version: string
19
+ }
20
+
21
+ interface SearchResponse {
22
+ skills: ClawHubSkill[]
23
+ total: number
24
+ page: number
25
+ }
6
26
 
7
27
  export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
8
28
  const skills = useAppStore((s) => s.skills)
9
29
  const loadSkills = useAppStore((s) => s.loadSkills)
10
30
  const setSkillSheetOpen = useAppStore((s) => s.setSkillSheetOpen)
11
31
  const setEditingSkillId = useAppStore((s) => s.setEditingSkillId)
32
+ const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
12
33
  const [clawHubOpen, setClawHubOpen] = useState(false)
13
34
 
35
+ // Embedded ClawHub state (full-width only)
36
+ const [tab, setTab] = useState<'skills' | 'clawhub'>('skills')
37
+ const [hubQuery, setHubQuery] = useState('')
38
+ const [hubSkills, setHubSkills] = useState<ClawHubSkill[]>([])
39
+ const [hubPage, setHubPage] = useState(1)
40
+ const [hubTotal, setHubTotal] = useState(0)
41
+ const [hubLoading, setHubLoading] = useState(false)
42
+ const [hubSearched, setHubSearched] = useState(false)
43
+ const [hubError, setHubError] = useState<string | null>(null)
44
+ const [installing, setInstalling] = useState<string | null>(null)
45
+
14
46
  useEffect(() => {
15
47
  loadSkills()
16
48
  }, [])
17
49
 
18
- const skillList = Object.values(skills)
50
+ const skillList = Object.values(skills).filter((s) => !activeProjectFilter || s.projectId === activeProjectFilter)
19
51
 
20
52
  const handleEdit = (id: string) => {
21
53
  setEditingSkillId(id)
22
54
  setSkillSheetOpen(true)
23
55
  }
24
56
 
25
- return (
26
- <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-4'}`}>
27
- <button
28
- onClick={() => setClawHubOpen(true)}
29
- className="w-full mb-3 py-2.5 px-4 rounded-[12px] border border-dashed border-white/[0.1] text-[13px] font-600 text-text-3 hover:text-accent-bright hover:border-accent-bright/30 transition-all cursor-pointer bg-transparent"
30
- style={{ fontFamily: 'inherit' }}
31
- >
32
- Browse ClawHub Skills
33
- </button>
34
- <ClawHubBrowser open={clawHubOpen} onOpenChange={setClawHubOpen} onInstalled={() => loadSkills()} />
35
- {skillList.length === 0 ? (
36
- <div className="text-center py-12">
37
- <p className="text-[13px] text-text-3/60">No skills yet</p>
57
+ const handleDelete = async (e: React.MouseEvent, id: string) => {
58
+ e.stopPropagation()
59
+ await api('DELETE', `/skills/${id}`)
60
+ loadSkills()
61
+ }
62
+
63
+ // Embedded ClawHub search
64
+ const searchHub = useCallback(async (q: string, p: number, append = false) => {
65
+ setHubLoading(true)
66
+ setHubError(null)
67
+ try {
68
+ const res = await api<SearchResponse>('GET', `/clawhub/search?q=${encodeURIComponent(q)}&page=${p}`)
69
+ if (append) {
70
+ setHubSkills(prev => [...prev, ...res.skills])
71
+ } else {
72
+ setHubSkills(res.skills)
73
+ }
74
+ setHubTotal(res.total)
75
+ setHubPage(res.page)
76
+ setHubSearched(true)
77
+ } catch (err) {
78
+ setHubError(err instanceof Error ? err.message : 'Failed to search ClawHub')
79
+ } finally {
80
+ setHubLoading(false)
81
+ }
82
+ }, [])
83
+
84
+ useEffect(() => {
85
+ if (!inSidebar && tab === 'clawhub' && !hubSearched) {
86
+ searchHub('', 1)
87
+ }
88
+ }, [tab, inSidebar, hubSearched, searchHub])
89
+
90
+ const handleHubSearch = () => {
91
+ setHubSkills([])
92
+ searchHub(hubQuery, 1)
93
+ }
94
+
95
+ const handleInstall = async (skill: ClawHubSkill) => {
96
+ setInstalling(skill.id)
97
+ try {
98
+ await api('POST', '/clawhub/install', {
99
+ name: skill.name,
100
+ description: skill.description,
101
+ url: skill.url,
102
+ tags: skill.tags,
103
+ })
104
+ toast.success(`Installed "${skill.name}"`)
105
+ loadSkills()
106
+ } catch (err) {
107
+ toast.error(err instanceof Error ? err.message : 'Install failed')
108
+ } finally {
109
+ setInstalling(null)
110
+ }
111
+ }
112
+
113
+ const tabClass = (t: string) =>
114
+ `py-1.5 px-3.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
115
+ ${tab === t
116
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
117
+ : 'bg-transparent border-transparent text-text-3 hover:text-text-2'}`
118
+
119
+ const renderClawHub = () => {
120
+ const hasMore = hubSkills.length < hubTotal
121
+
122
+ return (
123
+ <div className="space-y-3">
124
+ <div className="flex gap-2">
125
+ <input
126
+ placeholder="Search skills..."
127
+ value={hubQuery}
128
+ onChange={(e) => setHubQuery(e.target.value)}
129
+ onKeyDown={(e) => e.key === 'Enter' && handleHubSearch()}
130
+ className="flex-1 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"
131
+ style={{ fontFamily: 'inherit' }}
132
+ />
38
133
  <button
39
- onClick={() => setSkillSheetOpen(true)}
40
- 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"
134
+ onClick={handleHubSearch}
135
+ disabled={hubLoading}
136
+ className="px-3.5 py-2 rounded-[10px] text-[12px] font-600 bg-accent-soft text-accent-bright border border-accent-bright/20 hover:bg-accent-soft/80 transition-all cursor-pointer disabled:opacity-50"
41
137
  style={{ fontFamily: 'inherit' }}
42
138
  >
43
- + Add Skill
139
+ Search
44
140
  </button>
45
141
  </div>
46
- ) : (
47
- <div className="space-y-2">
48
- {skillList.map((skill) => (
142
+
143
+ {hubError && (
144
+ <div className="text-center py-8">
145
+ <p className="text-[13px] text-red-400">{hubError}</p>
146
+ <button onClick={() => searchHub(hubQuery, 1)} className="mt-2 text-[12px] text-text-3/60 hover:text-text-3 cursor-pointer bg-transparent border-none" style={{ fontFamily: 'inherit' }}>
147
+ Retry
148
+ </button>
149
+ </div>
150
+ )}
151
+
152
+ {!hubError && !hubLoading && hubSearched && hubSkills.length === 0 && (
153
+ <div className="text-center py-8">
154
+ <p className="text-[13px] text-text-3/60">No skills found</p>
155
+ {hubQuery && <p className="text-[11px] text-text-3/40 mt-1">Try a different search term</p>}
156
+ </div>
157
+ )}
158
+
159
+ {hubSkills.length > 0 && (
160
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
161
+ {hubSkills.map((skill) => (
162
+ <div
163
+ key={skill.id}
164
+ className="p-4 rounded-[14px] border border-white/[0.06] bg-surface"
165
+ >
166
+ <div className="flex items-start justify-between gap-2">
167
+ <div className="min-w-0 flex-1">
168
+ <div className="flex items-center gap-2 mb-0.5">
169
+ <span className="font-display text-[14px] font-600 text-text truncate">{skill.name}</span>
170
+ <span className="text-[10px] font-mono text-text-3/40 shrink-0">v{skill.version}</span>
171
+ </div>
172
+ <p className="text-[12px] text-text-3/60 line-clamp-2 mb-2">{skill.description}</p>
173
+ <div className="flex items-center gap-1.5 flex-wrap">
174
+ {skill.tags.slice(0, 4).map((tag) => (
175
+ <Badge key={tag} variant="secondary" className="text-[10px] px-1.5 py-0">{tag}</Badge>
176
+ ))}
177
+ </div>
178
+ <div className="flex items-center gap-3 mt-2 text-[11px] text-text-3/50">
179
+ <span>{skill.author}</span>
180
+ <span>{skill.downloads.toLocaleString()} installs</span>
181
+ </div>
182
+ </div>
183
+ <button
184
+ onClick={() => handleInstall(skill)}
185
+ disabled={installing === skill.id}
186
+ className="shrink-0 py-2 px-3.5 rounded-[10px] text-[12px] font-600 bg-accent-soft text-accent-bright border border-accent-bright/20 hover:bg-accent-soft/80 transition-all cursor-pointer disabled:opacity-50"
187
+ style={{ fontFamily: 'inherit' }}
188
+ >
189
+ {installing === skill.id ? 'Installing...' : 'Install'}
190
+ </button>
191
+ </div>
192
+ </div>
193
+ ))}
194
+ </div>
195
+ )}
196
+
197
+ {hasMore && (
198
+ <div className="pt-2 pb-4 text-center">
49
199
  <button
50
- key={skill.id}
51
- onClick={() => handleEdit(skill.id)}
52
- className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
200
+ onClick={() => searchHub(hubQuery, hubPage + 1, true)}
201
+ disabled={hubLoading}
202
+ className="text-[12px] text-text-3/60 hover:text-text-3 cursor-pointer bg-transparent border-none"
203
+ style={{ fontFamily: 'inherit' }}
53
204
  >
54
- <div className="flex items-center justify-between mb-1">
55
- <span className="font-display text-[14px] font-600 text-text truncate">{skill.name}</span>
56
- <span className="text-[10px] font-mono text-text-3/50 shrink-0 ml-2">{skill.filename}</span>
57
- </div>
58
- {skill.description && (
59
- <p className="text-[12px] text-text-3/60 line-clamp-2">{skill.description}</p>
60
- )}
61
- <div className="text-[11px] text-text-3/70 mt-1.5">
62
- {skill.content.length} chars
63
- </div>
205
+ {hubLoading ? 'Loading...' : 'Load More'}
64
206
  </button>
65
- ))}
207
+ </div>
208
+ )}
209
+
210
+ {hubLoading && hubSkills.length === 0 && (
211
+ <div className="flex items-center justify-center py-12">
212
+ <div className="h-5 w-5 animate-spin rounded-full border-2 border-text-3/20 border-t-text-3/60" />
213
+ </div>
214
+ )}
215
+ </div>
216
+ )
217
+ }
218
+
219
+ return (
220
+ <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
221
+ {/* Sidebar: ClawHub button + Sheet */}
222
+ {inSidebar && (
223
+ <>
224
+ <button
225
+ onClick={() => setClawHubOpen(true)}
226
+ className="w-full mb-3 py-2.5 px-4 rounded-[12px] border border-dashed border-white/[0.1] text-[13px] font-600 text-text-3 hover:text-accent-bright hover:border-accent-bright/30 transition-all cursor-pointer bg-transparent"
227
+ style={{ fontFamily: 'inherit' }}
228
+ >
229
+ Browse ClawHub Skills
230
+ </button>
231
+ <ClawHubBrowser open={clawHubOpen} onOpenChange={setClawHubOpen} onInstalled={() => loadSkills()} />
232
+ </>
233
+ )}
234
+
235
+ {/* Full-width: tabs */}
236
+ {!inSidebar && (
237
+ <div className="flex gap-1 mb-4">
238
+ <button onClick={() => setTab('skills')} className={tabClass('skills')} style={{ fontFamily: 'inherit' }}>
239
+ My Skills
240
+ </button>
241
+ <button onClick={() => setTab('clawhub')} className={tabClass('clawhub')} style={{ fontFamily: 'inherit' }}>
242
+ ClawHub
243
+ </button>
66
244
  </div>
67
245
  )}
246
+
247
+ {(!inSidebar && tab === 'clawhub') ? renderClawHub() : (
248
+ skillList.length === 0 ? (
249
+ <div className="text-center py-12">
250
+ <p className="text-[13px] text-text-3/60">No skills yet</p>
251
+ <button
252
+ onClick={() => setSkillSheetOpen(true)}
253
+ 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"
254
+ style={{ fontFamily: 'inherit' }}
255
+ >
256
+ + Add Skill
257
+ </button>
258
+ </div>
259
+ ) : (
260
+ <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
261
+ {skillList.map((skill) => (
262
+ <button
263
+ key={skill.id}
264
+ onClick={() => handleEdit(skill.id)}
265
+ className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
266
+ >
267
+ <div className="flex items-center justify-between mb-1">
268
+ <span className="font-display text-[14px] font-600 text-text truncate">{skill.name}</span>
269
+ <div className="flex items-center gap-2 shrink-0 ml-2">
270
+ <span className="text-[10px] font-mono text-text-3/50">{skill.filename}</span>
271
+ {!inSidebar && (
272
+ <button
273
+ onClick={(e) => handleDelete(e, skill.id)}
274
+ className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
275
+ title="Delete"
276
+ >
277
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
278
+ <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" />
279
+ </svg>
280
+ </button>
281
+ )}
282
+ </div>
283
+ </div>
284
+ {skill.description && (
285
+ <p className="text-[12px] text-text-3/60 line-clamp-2">{skill.description}</p>
286
+ )}
287
+ <div className="text-[11px] text-text-3/70 mt-1.5">
288
+ {skill.content.length} chars
289
+ </div>
290
+ </button>
291
+ ))}
292
+ </div>
293
+ )
294
+ )}
68
295
  </div>
69
296
  )
70
297
  }
@@ -3,7 +3,6 @@
3
3
  import { useEffect, useState, useRef } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
- import { AiGenBlock } from '@/components/shared/ai-gen-block'
7
6
  import { api } from '@/lib/api-client'
8
7
 
9
8
  export function SkillSheet() {
@@ -24,38 +23,8 @@ export function SkillSheet() {
24
23
  const [importError, setImportError] = useState('')
25
24
  const [importNotice, setImportNotice] = useState('')
26
25
 
27
- // AI generation state
28
- const [aiPrompt, setAiPrompt] = useState('')
29
- const [generating, setGenerating] = useState(false)
30
- const [generated, setGenerated] = useState(false)
31
- const [genError, setGenError] = useState('')
32
- const appSettings = useAppStore((s) => s.appSettings)
33
- const loadSettings = useAppStore((s) => s.loadSettings)
34
-
35
26
  const editing = editingId ? skills[editingId] : null
36
27
 
37
- const handleGenerate = async () => {
38
- if (!aiPrompt.trim()) return
39
- setGenerating(true)
40
- setGenError('')
41
- try {
42
- const result = await api<{ name?: string; description?: string; content?: string; error?: string }>('POST', '/generate', { type: 'skill', prompt: aiPrompt })
43
- if (result.error) {
44
- setGenError(result.error)
45
- } else if (result.name || result.content) {
46
- if (result.name) { setName(result.name); setFilename(`${result.name.toLowerCase().replace(/\s+/g, '-')}.md`) }
47
- if (result.description) setDescription(result.description)
48
- if (result.content) setContent(result.content)
49
- setGenerated(true)
50
- } else {
51
- setGenError('AI returned empty response — try again')
52
- }
53
- } catch (err: unknown) {
54
- setGenError(err instanceof Error ? err.message : 'Generation failed')
55
- }
56
- setGenerating(false)
57
- }
58
-
59
28
  const handleImportFromUrl = async () => {
60
29
  if (!importUrl.trim()) return
61
30
  setImportingUrl(true)
@@ -72,7 +41,6 @@ export function SkillSheet() {
72
41
  } else {
73
42
  setImportNotice('Skill imported from URL.')
74
43
  }
75
- setGenerated(false)
76
44
  } catch (err: unknown) {
77
45
  setImportError(err instanceof Error ? err.message : 'Failed to import skill URL')
78
46
  } finally {
@@ -82,11 +50,6 @@ export function SkillSheet() {
82
50
 
83
51
  useEffect(() => {
84
52
  if (open) {
85
- loadSettings()
86
- setAiPrompt('')
87
- setGenerating(false)
88
- setGenerated(false)
89
- setGenError('')
90
53
  setImportUrl('')
91
54
  setImportingUrl(false)
92
55
  setImportError('')
@@ -159,14 +122,6 @@ export function SkillSheet() {
159
122
  <p className="text-[14px] text-text-3">Upload or write a reusable instruction set for agents</p>
160
123
  </div>
161
124
 
162
- {/* AI Generation */}
163
- {!editing && <AiGenBlock
164
- aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
165
- generating={generating} generated={generated} genError={genError}
166
- onGenerate={handleGenerate} appSettings={appSettings}
167
- placeholder='Describe the skill, e.g. "A frontend design skill for building polished React components with Tailwind"'
168
- />}
169
-
170
125
  {/* File upload */}
171
126
  {!editing && (
172
127
  <div className="mb-8">
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useCallback, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { useWs } from '@/hooks/use-ws'
5
6
  import { updateTask } from '@/lib/tasks'
6
7
  import { TaskColumn } from './task-column'
7
8
  import type { BoardTaskStatus } from '@/types'
@@ -19,12 +20,8 @@ export function TaskBoard() {
19
20
  const setShowArchived = useAppStore((s) => s.setShowArchivedTasks)
20
21
  const [filterAgentId, setFilterAgentId] = useState<string>('')
21
22
 
22
- useEffect(() => {
23
- loadTasks()
24
- loadAgents()
25
- const interval = setInterval(loadTasks, 5000)
26
- return () => clearInterval(interval)
27
- }, [])
23
+ useEffect(() => { loadTasks(); loadAgents() }, [])
24
+ useWs('tasks', loadTasks, 5000)
28
25
 
29
26
  const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
30
27
 
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useState, useCallback } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { api } from '@/lib/api-client'
5
6
  import { updateTask, archiveTask } from '@/lib/tasks'
6
7
  import type { BoardTask, BoardTaskStatus } from '@/types'
7
8
 
@@ -40,7 +41,7 @@ export function TaskCard({ task }: { task: BoardTask }) {
40
41
  e.stopPropagation()
41
42
  if (task.sessionId) {
42
43
  setCurrentSession(task.sessionId)
43
- setActiveView('sessions')
44
+ setActiveView('agents')
44
45
  }
45
46
  }
46
47
 
@@ -161,6 +162,47 @@ export function TaskCard({ task }: { task: BoardTask }) {
161
162
  <p className="mt-2 text-[11px] text-red-400/80 line-clamp-2">{task.error}</p>
162
163
  )}
163
164
 
165
+ {/* Pending tool approval */}
166
+ {task.pendingApproval && (
167
+ <div className="mt-3 p-3 rounded-[10px] bg-amber-500/[0.08] border border-amber-500/20">
168
+ <div className="flex items-center gap-2 mb-2">
169
+ <svg className="w-3.5 h-3.5 text-amber-400" viewBox="0 0 16 16" fill="none">
170
+ <path d="M8 1l7 14H1L8 1z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
171
+ <path d="M8 6v3M8 11.5v.5" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
172
+ </svg>
173
+ <span className="text-[11px] font-600 text-amber-400">Approval Required</span>
174
+ </div>
175
+ <p className="text-[12px] text-text-2 mb-1 font-600">{task.pendingApproval.toolName}</p>
176
+ <pre className="text-[10px] text-text-3 bg-black/20 rounded-[6px] px-2 py-1.5 mb-2 overflow-x-auto max-h-[80px] overflow-y-auto whitespace-pre-wrap break-all">
177
+ {JSON.stringify(task.pendingApproval.args, null, 2).slice(0, 500)}
178
+ </pre>
179
+ <div className="flex gap-2">
180
+ <button
181
+ onClick={async (e) => {
182
+ e.stopPropagation()
183
+ await api('POST', `/tasks/${task.id}/approve`, { approved: true })
184
+ await loadTasks()
185
+ }}
186
+ className="flex-1 px-3 py-1.5 rounded-[8px] text-[11px] font-600 bg-green-500/20 text-green-400 border-none cursor-pointer hover:bg-green-500/30 transition-colors"
187
+ style={{ fontFamily: 'inherit' }}
188
+ >
189
+ Approve
190
+ </button>
191
+ <button
192
+ onClick={async (e) => {
193
+ e.stopPropagation()
194
+ await api('POST', `/tasks/${task.id}/approve`, { approved: false })
195
+ await loadTasks()
196
+ }}
197
+ className="flex-1 px-3 py-1.5 rounded-[8px] text-[11px] font-600 bg-red-500/20 text-red-400 border-none cursor-pointer hover:bg-red-500/30 transition-colors"
198
+ style={{ fontFamily: 'inherit' }}
199
+ >
200
+ Reject
201
+ </button>
202
+ </div>
203
+ </div>
204
+ )}
205
+
164
206
  {/* Inline comments — show latest 2 */}
165
207
  {task.comments && task.comments.length > 0 && (
166
208
  <div className="mt-3 pt-3 border-t border-white/[0.04] space-y-2">
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect, useMemo, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { useWs } from '@/hooks/use-ws'
5
6
  import { api } from '@/lib/api-client'
6
7
  import type { BoardTaskStatus } from '@/types'
7
8
 
@@ -20,18 +21,18 @@ export function TaskList({ inSidebar }: { inSidebar?: boolean }) {
20
21
  const agents = useAppStore((s) => s.agents)
21
22
  const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
22
23
  const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
24
+ const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
23
25
  const [search, setSearch] = useState('')
24
26
  const [clearing, setClearing] = useState(false)
25
27
 
26
- useEffect(() => {
27
- loadTasks()
28
- const interval = setInterval(loadTasks, 5000)
29
- return () => clearInterval(interval)
30
- }, [])
28
+ useEffect(() => { loadTasks() }, [])
29
+ useWs('tasks', loadTasks, 5000)
31
30
 
32
31
  const sorted = useMemo(() =>
33
- Object.values(tasks).sort((a, b) => b.updatedAt - a.updatedAt),
34
- [tasks],
32
+ Object.values(tasks)
33
+ .filter((t) => !activeProjectFilter || t.projectId === activeProjectFilter)
34
+ .sort((a, b) => b.updatedAt - a.updatedAt),
35
+ [tasks, activeProjectFilter],
35
36
  )
36
37
 
37
38
  const filtered = useMemo(() => {
@@ -3,9 +3,7 @@
3
3
  import { useEffect, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { createTask, updateTask, archiveTask, unarchiveTask } from '@/lib/tasks'
6
- import { api } from '@/lib/api-client'
7
6
  import { BottomSheet } from '@/components/shared/bottom-sheet'
8
- import { AiGenBlock } from '@/components/shared/ai-gen-block'
9
7
  import { DirBrowser } from '@/components/shared/dir-browser'
10
8
  import type { BoardTask, TaskComment } from '@/types'
11
9
 
@@ -36,46 +34,12 @@ export function TaskSheet() {
36
34
  const [cwd, setCwd] = useState('')
37
35
  const [file, setFile] = useState<string | null>(null)
38
36
 
39
- // AI generation state
40
- const [aiPrompt, setAiPrompt] = useState('')
41
- const [generating, setGenerating] = useState(false)
42
- const [generated, setGenerated] = useState(false)
43
- const [genError, setGenError] = useState('')
44
- const appSettings = useAppStore((s) => s.appSettings)
45
- const loadSettings = useAppStore((s) => s.loadSettings)
46
-
47
37
  const editing = editingId ? tasks[editingId] : null
48
38
  const orchestrators = Object.values(agents).filter((p) => p.isOrchestrator)
49
39
 
50
- const handleGenerate = async () => {
51
- if (!aiPrompt.trim()) return
52
- setGenerating(true)
53
- setGenError('')
54
- try {
55
- const result = await api<{ title?: string; description?: string; error?: string }>('POST', '/generate', { type: 'task', prompt: aiPrompt })
56
- if (result.error) {
57
- setGenError(result.error)
58
- } else if (result.title || result.description) {
59
- if (result.title) setTitle(result.title)
60
- if (result.description) setDescription(result.description)
61
- setGenerated(true)
62
- } else {
63
- setGenError('AI returned empty response — try again')
64
- }
65
- } catch (err: unknown) {
66
- setGenError(err instanceof Error ? err.message : 'Generation failed')
67
- }
68
- setGenerating(false)
69
- }
70
-
71
40
  useEffect(() => {
72
41
  if (open) {
73
42
  loadAgents()
74
- loadSettings()
75
- setAiPrompt('')
76
- setGenerating(false)
77
- setGenerated(false)
78
- setGenError('')
79
43
  if (editing) {
80
44
  setTitle(editing.title)
81
45
  setDescription(editing.description)
@@ -185,14 +149,6 @@ export function TaskSheet() {
185
149
  </p>
186
150
  </div>
187
151
 
188
- {/* AI Generation */}
189
- {!editing && <AiGenBlock
190
- aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
191
- generating={generating} generated={generated} genError={genError}
192
- onGenerate={handleGenerate} appSettings={appSettings}
193
- placeholder='Describe the task, e.g. "Audit all pages on example.com for SEO issues and broken links"'
194
- />}
195
-
196
152
  <div className="mb-8">
197
153
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Title</label>
198
154
  <input
@@ -52,9 +52,9 @@ export function UsageList() {
52
52
  }
53
53
 
54
54
  return (
55
- <div className="flex-1 overflow-y-auto px-3 pb-20">
55
+ <div className="flex-1 overflow-y-auto px-5 pb-8">
56
56
  {/* Summary */}
57
- <div className="grid grid-cols-2 gap-2 mb-4 mt-1">
57
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4 mt-1">
58
58
  <div className="p-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
59
59
  <div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Total Cost</div>
60
60
  <div className="text-[18px] font-700 text-text tracking-tight">{formatCost(data.totalCost)}</div>
@@ -63,15 +63,23 @@ export function UsageList() {
63
63
  <div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Total Tokens</div>
64
64
  <div className="text-[18px] font-700 text-text tracking-tight">{formatTokens(data.totalTokens)}</div>
65
65
  </div>
66
+ <div className="p-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
67
+ <div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Total Requests</div>
68
+ <div className="text-[18px] font-700 text-text tracking-tight">{providers.reduce((sum, [, s]) => sum + s.requests, 0)}</div>
69
+ </div>
70
+ <div className="p-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06]">
71
+ <div className="text-[10px] font-600 text-text-3 uppercase tracking-wider mb-1">Providers</div>
72
+ <div className="text-[18px] font-700 text-text tracking-tight">{providers.length}</div>
73
+ </div>
66
74
  </div>
67
75
 
68
76
  {/* Provider breakdown */}
69
77
  <div className="mb-2">
70
- <h3 className="text-[11px] font-600 text-text-3 uppercase tracking-wider px-1 mb-2">By Provider</h3>
78
+ <h3 className="text-[11px] font-600 text-text-3 uppercase tracking-wider mb-2">By Provider</h3>
71
79
  {providers.length === 0 ? (
72
80
  <div className="text-center py-6 text-[12px] text-text-3/60">No usage data yet</div>
73
81
  ) : (
74
- <div className="space-y-1.5">
82
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
75
83
  {providers.map(([provider, stats]) => {
76
84
  const pct = data.totalCost > 0 ? (stats.cost / data.totalCost) * 100 : 0
77
85
  return (