@swarmclawai/swarmclaw 0.3.0 → 0.4.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 (118) hide show
  1. package/README.md +20 -11
  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 +2 -0
  6. package/package.json +3 -1
  7. package/src/app/api/agents/[id]/route.ts +3 -0
  8. package/src/app/api/agents/[id]/thread/route.ts +2 -1
  9. package/src/app/api/agents/route.ts +5 -1
  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/connectors/[id]/route.ts +4 -0
  13. package/src/app/api/connectors/route.ts +6 -1
  14. package/src/app/api/credentials/route.ts +3 -1
  15. package/src/app/api/daemon/route.ts +6 -1
  16. package/src/app/api/ip/route.ts +3 -1
  17. package/src/app/api/mcp-servers/route.ts +3 -1
  18. package/src/app/api/orchestrator/graph/route.ts +25 -0
  19. package/src/app/api/plugins/marketplace/route.ts +3 -1
  20. package/src/app/api/plugins/route.ts +3 -1
  21. package/src/app/api/providers/[id]/route.ts +3 -0
  22. package/src/app/api/providers/configs/route.ts +3 -1
  23. package/src/app/api/providers/route.ts +5 -1
  24. package/src/app/api/schedules/[id]/route.ts +3 -0
  25. package/src/app/api/schedules/route.ts +6 -1
  26. package/src/app/api/secrets/route.ts +3 -1
  27. package/src/app/api/sessions/[id]/chat/route.ts +5 -2
  28. package/src/app/api/sessions/route.ts +9 -2
  29. package/src/app/api/settings/route.ts +3 -1
  30. package/src/app/api/setup/doctor/route.ts +1 -0
  31. package/src/app/api/setup/openclaw-device/route.ts +3 -1
  32. package/src/app/api/skills/route.ts +3 -1
  33. package/src/app/api/tasks/[id]/approve/route.ts +73 -0
  34. package/src/app/api/tasks/[id]/route.ts +3 -0
  35. package/src/app/api/tasks/route.ts +3 -0
  36. package/src/app/api/usage/route.ts +3 -1
  37. package/src/app/api/version/route.ts +3 -1
  38. package/src/app/api/webhooks/[id]/route.ts +2 -1
  39. package/src/app/api/webhooks/route.ts +3 -1
  40. package/src/app/icon.svg +58 -0
  41. package/src/app/page.tsx +8 -2
  42. package/src/cli/index.js +1 -9
  43. package/src/cli/index.ts +51 -1
  44. package/src/cli/spec.js +0 -8
  45. package/src/components/agents/agent-card.tsx +1 -1
  46. package/src/components/agents/agent-sheet.tsx +63 -80
  47. package/src/components/chat/chat-area.tsx +44 -30
  48. package/src/components/chat/chat-tool-toggles.tsx +12 -53
  49. package/src/components/chat/message-bubble.tsx +110 -42
  50. package/src/components/chat/tool-call-bubble.tsx +41 -3
  51. package/src/components/chat/tool-request-banner.tsx +1 -9
  52. package/src/components/connectors/connector-list.tsx +3 -8
  53. package/src/components/connectors/connector-sheet.tsx +24 -29
  54. package/src/components/input/chat-input.tsx +72 -56
  55. package/src/components/knowledge/knowledge-list.tsx +27 -31
  56. package/src/components/layout/app-layout.tsx +92 -71
  57. package/src/components/layout/daemon-indicator.tsx +3 -5
  58. package/src/components/logs/log-list.tsx +5 -9
  59. package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
  60. package/src/components/memory/memory-detail.tsx +1 -1
  61. package/src/components/plugins/plugin-list.tsx +227 -27
  62. package/src/components/providers/provider-list.tsx +46 -13
  63. package/src/components/providers/provider-sheet.tsx +0 -45
  64. package/src/components/runs/run-list.tsx +6 -15
  65. package/src/components/schedules/schedule-card.tsx +54 -4
  66. package/src/components/schedules/schedule-list.tsx +6 -3
  67. package/src/components/schedules/schedule-sheet.tsx +0 -47
  68. package/src/components/secrets/secrets-list.tsx +20 -2
  69. package/src/components/sessions/new-session-sheet.tsx +8 -9
  70. package/src/components/shared/connector-platform-icon.tsx +22 -20
  71. package/src/components/shared/model-combobox.tsx +148 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +7 -39
  73. package/src/components/shared/settings/section-orchestrator.tsx +8 -9
  74. package/src/components/skills/skill-list.tsx +260 -34
  75. package/src/components/skills/skill-sheet.tsx +0 -45
  76. package/src/components/tasks/task-board.tsx +3 -6
  77. package/src/components/tasks/task-card.tsx +43 -1
  78. package/src/components/tasks/task-list.tsx +3 -5
  79. package/src/components/tasks/task-sheet.tsx +0 -44
  80. package/src/components/usage/usage-list.tsx +12 -4
  81. package/src/hooks/use-ws.ts +66 -0
  82. package/src/instrumentation.ts +2 -0
  83. package/src/lib/chat.ts +14 -2
  84. package/src/lib/providers/anthropic.ts +1 -1
  85. package/src/lib/providers/index.ts +2 -0
  86. package/src/lib/providers/ollama.ts +1 -1
  87. package/src/lib/providers/openai.ts +33 -12
  88. package/src/lib/server/chat-execution.ts +19 -4
  89. package/src/lib/server/connectors/manager.ts +9 -3
  90. package/src/lib/server/context-manager.ts +1 -1
  91. package/src/lib/server/daemon-state.ts +3 -0
  92. package/src/lib/server/data-dir.ts +1 -0
  93. package/src/lib/server/heartbeat-service.ts +67 -3
  94. package/src/lib/server/langgraph-checkpoint.ts +274 -0
  95. package/src/lib/server/main-agent-loop.ts +61 -2
  96. package/src/lib/server/orchestrator-lg.ts +394 -13
  97. package/src/lib/server/orchestrator.ts +25 -5
  98. package/src/lib/server/queue.ts +17 -3
  99. package/src/lib/server/session-run-manager.ts +6 -1
  100. package/src/lib/server/session-tools/delegate.ts +2 -2
  101. package/src/lib/server/session-tools/index.ts +2 -0
  102. package/src/lib/server/session-tools/sandbox.ts +164 -0
  103. package/src/lib/server/storage-mcp.test.ts +25 -2
  104. package/src/lib/server/storage.ts +24 -7
  105. package/src/lib/server/stream-agent-chat.ts +77 -22
  106. package/src/lib/server/task-validation.test.ts +23 -0
  107. package/src/lib/server/task-validation.ts +5 -3
  108. package/src/lib/server/ws-hub.ts +85 -0
  109. package/src/lib/tool-definitions.ts +42 -0
  110. package/src/lib/upload.ts +7 -1
  111. package/src/lib/ws-client.ts +124 -0
  112. package/src/stores/use-chat-store.ts +33 -13
  113. package/src/types/index.ts +8 -1
  114. package/src/app/api/agents/generate/route.ts +0 -42
  115. package/src/app/api/generate/info/route.ts +0 -12
  116. package/src/app/api/generate/route.ts +0 -106
  117. package/src/app/favicon.ico +0 -0
  118. package/src/components/shared/ai-gen-block.tsx +0 -77
@@ -1,8 +1,28 @@
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)
@@ -11,6 +31,17 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
11
31
  const setEditingSkillId = useAppStore((s) => s.setEditingSkillId)
12
32
  const [clawHubOpen, setClawHubOpen] = useState(false)
13
33
 
34
+ // Embedded ClawHub state (full-width only)
35
+ const [tab, setTab] = useState<'skills' | 'clawhub'>('skills')
36
+ const [hubQuery, setHubQuery] = useState('')
37
+ const [hubSkills, setHubSkills] = useState<ClawHubSkill[]>([])
38
+ const [hubPage, setHubPage] = useState(1)
39
+ const [hubTotal, setHubTotal] = useState(0)
40
+ const [hubLoading, setHubLoading] = useState(false)
41
+ const [hubSearched, setHubSearched] = useState(false)
42
+ const [hubError, setHubError] = useState<string | null>(null)
43
+ const [installing, setInstalling] = useState<string | null>(null)
44
+
14
45
  useEffect(() => {
15
46
  loadSkills()
16
47
  }, [])
@@ -22,49 +53,244 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
22
53
  setSkillSheetOpen(true)
23
54
  }
24
55
 
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>
56
+ const handleDelete = async (e: React.MouseEvent, id: string) => {
57
+ e.stopPropagation()
58
+ await api('DELETE', `/skills/${id}`)
59
+ loadSkills()
60
+ }
61
+
62
+ // Embedded ClawHub search
63
+ const searchHub = useCallback(async (q: string, p: number, append = false) => {
64
+ setHubLoading(true)
65
+ setHubError(null)
66
+ try {
67
+ const res = await api<SearchResponse>('GET', `/clawhub/search?q=${encodeURIComponent(q)}&page=${p}`)
68
+ if (append) {
69
+ setHubSkills(prev => [...prev, ...res.skills])
70
+ } else {
71
+ setHubSkills(res.skills)
72
+ }
73
+ setHubTotal(res.total)
74
+ setHubPage(res.page)
75
+ setHubSearched(true)
76
+ } catch (err) {
77
+ setHubError(err instanceof Error ? err.message : 'Failed to search ClawHub')
78
+ } finally {
79
+ setHubLoading(false)
80
+ }
81
+ }, [])
82
+
83
+ useEffect(() => {
84
+ if (!inSidebar && tab === 'clawhub' && !hubSearched) {
85
+ searchHub('', 1)
86
+ }
87
+ }, [tab, inSidebar, hubSearched, searchHub])
88
+
89
+ const handleHubSearch = () => {
90
+ setHubSkills([])
91
+ searchHub(hubQuery, 1)
92
+ }
93
+
94
+ const handleInstall = async (skill: ClawHubSkill) => {
95
+ setInstalling(skill.id)
96
+ try {
97
+ await api('POST', '/clawhub/install', {
98
+ name: skill.name,
99
+ description: skill.description,
100
+ url: skill.url,
101
+ tags: skill.tags,
102
+ })
103
+ toast.success(`Installed "${skill.name}"`)
104
+ loadSkills()
105
+ } catch (err) {
106
+ toast.error(err instanceof Error ? err.message : 'Install failed')
107
+ } finally {
108
+ setInstalling(null)
109
+ }
110
+ }
111
+
112
+ const tabClass = (t: string) =>
113
+ `py-1.5 px-3.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border
114
+ ${tab === t
115
+ ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
116
+ : 'bg-transparent border-transparent text-text-3 hover:text-text-2'}`
117
+
118
+ const renderClawHub = () => {
119
+ const hasMore = hubSkills.length < hubTotal
120
+
121
+ return (
122
+ <div className="space-y-3">
123
+ <div className="flex gap-2">
124
+ <input
125
+ placeholder="Search skills..."
126
+ value={hubQuery}
127
+ onChange={(e) => setHubQuery(e.target.value)}
128
+ onKeyDown={(e) => e.key === 'Enter' && handleHubSearch()}
129
+ 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"
130
+ style={{ fontFamily: 'inherit' }}
131
+ />
38
132
  <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"
133
+ onClick={handleHubSearch}
134
+ disabled={hubLoading}
135
+ 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
136
  style={{ fontFamily: 'inherit' }}
42
137
  >
43
- + Add Skill
138
+ Search
44
139
  </button>
45
140
  </div>
46
- ) : (
47
- <div className="space-y-2">
48
- {skillList.map((skill) => (
141
+
142
+ {hubError && (
143
+ <div className="text-center py-8">
144
+ <p className="text-[13px] text-red-400">{hubError}</p>
145
+ <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' }}>
146
+ Retry
147
+ </button>
148
+ </div>
149
+ )}
150
+
151
+ {!hubError && !hubLoading && hubSearched && hubSkills.length === 0 && (
152
+ <div className="text-center py-8">
153
+ <p className="text-[13px] text-text-3/60">No skills found</p>
154
+ {hubQuery && <p className="text-[11px] text-text-3/40 mt-1">Try a different search term</p>}
155
+ </div>
156
+ )}
157
+
158
+ {hubSkills.length > 0 && (
159
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
160
+ {hubSkills.map((skill) => (
161
+ <div
162
+ key={skill.id}
163
+ className="p-4 rounded-[14px] border border-white/[0.06] bg-surface"
164
+ >
165
+ <div className="flex items-start justify-between gap-2">
166
+ <div className="min-w-0 flex-1">
167
+ <div className="flex items-center gap-2 mb-0.5">
168
+ <span className="font-display text-[14px] font-600 text-text truncate">{skill.name}</span>
169
+ <span className="text-[10px] font-mono text-text-3/40 shrink-0">v{skill.version}</span>
170
+ </div>
171
+ <p className="text-[12px] text-text-3/60 line-clamp-2 mb-2">{skill.description}</p>
172
+ <div className="flex items-center gap-1.5 flex-wrap">
173
+ {skill.tags.slice(0, 4).map((tag) => (
174
+ <Badge key={tag} variant="secondary" className="text-[10px] px-1.5 py-0">{tag}</Badge>
175
+ ))}
176
+ </div>
177
+ <div className="flex items-center gap-3 mt-2 text-[11px] text-text-3/50">
178
+ <span>{skill.author}</span>
179
+ <span>{skill.downloads.toLocaleString()} installs</span>
180
+ </div>
181
+ </div>
182
+ <button
183
+ onClick={() => handleInstall(skill)}
184
+ disabled={installing === skill.id}
185
+ 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"
186
+ style={{ fontFamily: 'inherit' }}
187
+ >
188
+ {installing === skill.id ? 'Installing...' : 'Install'}
189
+ </button>
190
+ </div>
191
+ </div>
192
+ ))}
193
+ </div>
194
+ )}
195
+
196
+ {hasMore && (
197
+ <div className="pt-2 pb-4 text-center">
49
198
  <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"
199
+ onClick={() => searchHub(hubQuery, hubPage + 1, true)}
200
+ disabled={hubLoading}
201
+ className="text-[12px] text-text-3/60 hover:text-text-3 cursor-pointer bg-transparent border-none"
202
+ style={{ fontFamily: 'inherit' }}
53
203
  >
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>
204
+ {hubLoading ? 'Loading...' : 'Load More'}
64
205
  </button>
65
- ))}
206
+ </div>
207
+ )}
208
+
209
+ {hubLoading && hubSkills.length === 0 && (
210
+ <div className="flex items-center justify-center py-12">
211
+ <div className="h-5 w-5 animate-spin rounded-full border-2 border-text-3/20 border-t-text-3/60" />
212
+ </div>
213
+ )}
214
+ </div>
215
+ )
216
+ }
217
+
218
+ return (
219
+ <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
220
+ {/* Sidebar: ClawHub button + Sheet */}
221
+ {inSidebar && (
222
+ <>
223
+ <button
224
+ onClick={() => setClawHubOpen(true)}
225
+ 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"
226
+ style={{ fontFamily: 'inherit' }}
227
+ >
228
+ Browse ClawHub Skills
229
+ </button>
230
+ <ClawHubBrowser open={clawHubOpen} onOpenChange={setClawHubOpen} onInstalled={() => loadSkills()} />
231
+ </>
232
+ )}
233
+
234
+ {/* Full-width: tabs */}
235
+ {!inSidebar && (
236
+ <div className="flex gap-1 mb-4">
237
+ <button onClick={() => setTab('skills')} className={tabClass('skills')} style={{ fontFamily: 'inherit' }}>
238
+ My Skills
239
+ </button>
240
+ <button onClick={() => setTab('clawhub')} className={tabClass('clawhub')} style={{ fontFamily: 'inherit' }}>
241
+ ClawHub
242
+ </button>
66
243
  </div>
67
244
  )}
245
+
246
+ {(!inSidebar && tab === 'clawhub') ? renderClawHub() : (
247
+ skillList.length === 0 ? (
248
+ <div className="text-center py-12">
249
+ <p className="text-[13px] text-text-3/60">No skills yet</p>
250
+ <button
251
+ onClick={() => setSkillSheetOpen(true)}
252
+ 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"
253
+ style={{ fontFamily: 'inherit' }}
254
+ >
255
+ + Add Skill
256
+ </button>
257
+ </div>
258
+ ) : (
259
+ <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
260
+ {skillList.map((skill) => (
261
+ <button
262
+ key={skill.id}
263
+ onClick={() => handleEdit(skill.id)}
264
+ className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
265
+ >
266
+ <div className="flex items-center justify-between mb-1">
267
+ <span className="font-display text-[14px] font-600 text-text truncate">{skill.name}</span>
268
+ <div className="flex items-center gap-2 shrink-0 ml-2">
269
+ <span className="text-[10px] font-mono text-text-3/50">{skill.filename}</span>
270
+ {!inSidebar && (
271
+ <button
272
+ onClick={(e) => handleDelete(e, skill.id)}
273
+ className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
274
+ title="Delete"
275
+ >
276
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
277
+ <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" />
278
+ </svg>
279
+ </button>
280
+ )}
281
+ </div>
282
+ </div>
283
+ {skill.description && (
284
+ <p className="text-[12px] text-text-3/60 line-clamp-2">{skill.description}</p>
285
+ )}
286
+ <div className="text-[11px] text-text-3/70 mt-1.5">
287
+ {skill.content.length} chars
288
+ </div>
289
+ </button>
290
+ ))}
291
+ </div>
292
+ )
293
+ )}
68
294
  </div>
69
295
  )
70
296
  }
@@ -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
 
@@ -23,11 +24,8 @@ export function TaskList({ inSidebar }: { inSidebar?: boolean }) {
23
24
  const [search, setSearch] = useState('')
24
25
  const [clearing, setClearing] = useState(false)
25
26
 
26
- useEffect(() => {
27
- loadTasks()
28
- const interval = setInterval(loadTasks, 5000)
29
- return () => clearInterval(interval)
30
- }, [])
27
+ useEffect(() => { loadTasks() }, [])
28
+ useWs('tasks', loadTasks, 5000)
31
29
 
32
30
  const sorted = useMemo(() =>
33
31
  Object.values(tasks).sort((a, b) => b.updatedAt - a.updatedAt),
@@ -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 (