@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
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { useCallback, useEffect, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { useWs } from '@/hooks/use-ws'
6
+ import { api } from '@/lib/api-client'
5
7
 
6
8
  export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
7
9
  const providers = useAppStore((s) => s.providers)
@@ -19,20 +21,26 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
19
21
  setLoaded(true)
20
22
  }, [loadProviders, loadProviderConfigs, loadCredentials])
21
23
 
22
- useEffect(() => {
23
- const bootstrap = setTimeout(() => { void refresh() }, 0)
24
- const poll = setInterval(() => { void loadProviders() }, 20_000)
25
- return () => {
26
- clearTimeout(bootstrap)
27
- clearInterval(poll)
28
- }
29
- }, [refresh, loadProviders])
24
+ useEffect(() => { void refresh() }, [refresh])
25
+ useWs('providers', loadProviders, 20_000)
30
26
 
31
27
  const handleEdit = (id: string) => {
32
28
  setEditingProviderId(id)
33
29
  setProviderSheetOpen(true)
34
30
  }
35
31
 
32
+ const handleToggle = async (e: React.MouseEvent, id: string, currentEnabled: boolean) => {
33
+ e.stopPropagation()
34
+ await api('PUT', `/providers/${id}`, { isEnabled: !currentEnabled })
35
+ await loadProviderConfigs()
36
+ }
37
+
38
+ const handleDelete = async (e: React.MouseEvent, id: string) => {
39
+ e.stopPropagation()
40
+ await api('DELETE', `/providers/${id}`)
41
+ await loadProviderConfigs()
42
+ }
43
+
36
44
  // Merge built-in providers with custom configs
37
45
  const builtinItems = providers.map((p) => ({
38
46
  id: p.id,
@@ -58,15 +66,15 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
58
66
 
59
67
  if (!loaded) {
60
68
  return (
61
- <div className={`flex-1 flex items-center justify-center ${inSidebar ? 'px-3 pb-4' : 'px-4'}`}>
69
+ <div className={`flex-1 flex items-center justify-center ${inSidebar ? 'px-3 pb-4' : 'px-5'}`}>
62
70
  <p className="text-[13px] text-text-3">Loading providers...</p>
63
71
  </div>
64
72
  )
65
73
  }
66
74
 
67
75
  return (
68
- <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-4'}`}>
69
- <div className="space-y-2">
76
+ <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
77
+ <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
70
78
  {allItems.map((item) => (
71
79
  <button
72
80
  key={item.id}
@@ -81,12 +89,37 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
81
89
  ${item.type === 'builtin' ? 'bg-white/[0.04] text-text-3' : 'bg-[#6366F1]/10 text-[#6366F1]'}`}>
82
90
  {item.type === 'builtin' ? 'Built-in' : 'Custom'}
83
91
  </span>
92
+ {!inSidebar && item.type === 'custom' && (
93
+ <>
94
+ <div
95
+ onClick={(e) => handleToggle(e, item.id, item.isEnabled)}
96
+ className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
97
+ ${item.isEnabled ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
98
+ >
99
+ <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
100
+ ${item.isEnabled ? 'left-[18px]' : 'left-0.5'}`} />
101
+ </div>
102
+ <button
103
+ onClick={(e) => handleDelete(e, item.id)}
104
+ className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
105
+ title="Delete provider"
106
+ >
107
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
108
+ <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" />
109
+ </svg>
110
+ </button>
111
+ </>
112
+ )}
84
113
  <span className={`w-2 h-2 rounded-full ${item.isConnected ? 'bg-emerald-400' : 'bg-white/10'}`} />
85
114
  </div>
86
115
  </div>
87
116
  <div className="text-[12px] text-text-3/60 font-mono truncate">
88
- {item.models.slice(0, 3).join(', ')}
89
- {item.models.length > 3 && ` +${item.models.length - 3}`}
117
+ {!inSidebar ? item.models.join(', ') : (
118
+ <>
119
+ {item.models.slice(0, 3).join(', ')}
120
+ {item.models.length > 3 && ` +${item.models.length - 3}`}
121
+ </>
122
+ )}
90
123
  </div>
91
124
  </button>
92
125
  ))}
@@ -5,7 +5,6 @@ import { useAppStore } from '@/stores/use-app-store'
5
5
  import { createProviderConfig, updateProviderConfig, deleteProviderConfig } from '@/lib/provider-config'
6
6
  import { api } from '@/lib/api-client'
7
7
  import { BottomSheet } from '@/components/shared/bottom-sheet'
8
- import { AiGenBlock } from '@/components/shared/ai-gen-block'
9
8
  import { toast } from 'sonner'
10
9
 
11
10
  export function ProviderSheet() {
@@ -41,51 +40,15 @@ export function ProviderSheet() {
41
40
  const [localLoading, setLocalLoading] = useState(false)
42
41
  const [localError, setLocalError] = useState('')
43
42
 
44
- // AI generation state
45
- const [aiPrompt, setAiPrompt] = useState('')
46
- const [generating, setGenerating] = useState(false)
47
- const [generated, setGenerated] = useState(false)
48
- const [genError, setGenError] = useState('')
49
- const appSettings = useAppStore((s) => s.appSettings)
50
- const loadSettings = useAppStore((s) => s.loadSettings)
51
-
52
43
  // Find editing provider in custom configs OR built-in list
53
44
  const editingCustom = editingId ? providerConfigs.find((c) => c.id === editingId) : null
54
45
  const editingBuiltin = editingId ? providers.find((p) => p.id === editingId) : null
55
46
  const isBuiltin = !!editingBuiltin && !editingCustom
56
47
  const editing = editingCustom || editingBuiltin
57
48
 
58
- const handleGenerate = async () => {
59
- if (!aiPrompt.trim()) return
60
- setGenerating(true)
61
- setGenError('')
62
- try {
63
- const result = await api<{ name?: string; baseUrl?: string; models?: string; requiresApiKey?: boolean; error?: string }>('POST', '/generate', { type: 'provider', prompt: aiPrompt })
64
- if (result.error) {
65
- setGenError(result.error)
66
- } else if (result.name || result.baseUrl) {
67
- if (result.name) setName(result.name)
68
- if (result.baseUrl) setBaseUrl(result.baseUrl)
69
- if (result.models) setModels(result.models)
70
- if (result.requiresApiKey !== undefined) setRequiresApiKey(result.requiresApiKey)
71
- setGenerated(true)
72
- } else {
73
- setGenError('AI returned empty response — try again')
74
- }
75
- } catch (err: unknown) {
76
- setGenError(err instanceof Error ? err.message : 'Generation failed')
77
- }
78
- setGenerating(false)
79
- }
80
-
81
49
  useEffect(() => {
82
50
  if (open) {
83
51
  loadCredentials()
84
- loadSettings()
85
- setAiPrompt('')
86
- setGenerating(false)
87
- setGenerated(false)
88
- setGenError('')
89
52
  setNewModel('')
90
53
  setLocalModels([])
91
54
  setLocalError('')
@@ -248,14 +211,6 @@ export function ProviderSheet() {
248
211
  </p>
249
212
  </div>
250
213
 
251
- {/* AI Generation — only for new custom providers */}
252
- {isNew && <AiGenBlock
253
- aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
254
- generating={generating} generated={generated} genError={genError}
255
- onGenerate={handleGenerate} appSettings={appSettings}
256
- placeholder='Name a provider, e.g. "Groq", "Together AI", "z.ai", "DeepSeek"'
257
- />}
258
-
259
214
  {/* Name */}
260
215
  <div className="mb-8">
261
216
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
@@ -1,7 +1,8 @@
1
1
  'use client'
2
2
 
3
- import { useEffect, useState, useRef, useCallback } from 'react'
3
+ import { useEffect, useState, useCallback } from 'react'
4
4
  import { api } from '@/lib/api-client'
5
+ import { useWs } from '@/hooks/use-ws'
5
6
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
7
  import type { SessionRunRecord, SessionRunStatus } from '@/types'
7
8
 
@@ -37,7 +38,6 @@ export function RunList() {
37
38
  const [autoRefresh, setAutoRefresh] = useState(false)
38
39
  const [statusFilter, setStatusFilter] = useState<SessionRunStatus | null>(null)
39
40
  const [selected, setSelected] = useState<SessionRunRecord | null>(null)
40
- const timerRef = useRef<ReturnType<typeof setInterval> | null>(null)
41
41
 
42
42
  const fetchRuns = useCallback(async () => {
43
43
  try {
@@ -54,16 +54,7 @@ export function RunList() {
54
54
  fetchRuns()
55
55
  }, [fetchRuns])
56
56
 
57
- useEffect(() => {
58
- if (!autoRefresh) {
59
- if (timerRef.current) clearInterval(timerRef.current)
60
- return
61
- }
62
- timerRef.current = setInterval(fetchRuns, 3000)
63
- return () => {
64
- if (timerRef.current) clearInterval(timerRef.current)
65
- }
66
- }, [autoRefresh, fetchRuns])
57
+ useWs('runs', fetchRuns, autoRefresh ? 3000 : undefined)
67
58
 
68
59
  const filtered = statusFilter ? runs.filter((r) => r.status === statusFilter) : runs
69
60
 
@@ -78,7 +69,7 @@ export function RunList() {
78
69
  return (
79
70
  <div className="flex-1 flex flex-col overflow-hidden">
80
71
  {/* Controls */}
81
- <div className="px-4 py-2 space-y-2 shrink-0">
72
+ <div className="px-5 py-2 space-y-2 shrink-0">
82
73
  {/* Status filter + auto-refresh */}
83
74
  <div className="flex items-center gap-1.5 flex-wrap">
84
75
  <button
@@ -113,12 +104,12 @@ export function RunList() {
113
104
  </div>
114
105
 
115
106
  {/* Count */}
116
- <div className="px-4 py-1 text-[10px] text-text-3/60">
107
+ <div className="px-5 py-1 text-[10px] text-text-3/60">
117
108
  {filtered.length} run{filtered.length !== 1 ? 's' : ''}
118
109
  </div>
119
110
 
120
111
  {/* Run list */}
121
- <div className="flex-1 overflow-y-auto px-2 pb-20">
112
+ <div className="flex-1 overflow-y-auto px-4 pb-8">
122
113
  {filtered.length === 0 ? (
123
114
  <div className="flex items-center justify-center h-32 text-text-3 text-[12px]">
124
115
  No runs found
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { Schedule } from '@/types'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { api } from '@/lib/api-client'
5
6
 
6
7
  const STATUS_COLORS: Record<string, string> = {
7
8
  active: 'text-emerald-400 bg-emerald-400/[0.08]',
@@ -24,11 +25,13 @@ function formatNext(ts?: number): string {
24
25
 
25
26
  interface Props {
26
27
  schedule: Schedule
28
+ inSidebar?: boolean
27
29
  }
28
30
 
29
- export function ScheduleCard({ schedule }: Props) {
31
+ export function ScheduleCard({ schedule, inSidebar }: Props) {
30
32
  const setEditingScheduleId = useAppStore((s) => s.setEditingScheduleId)
31
33
  const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
34
+ const loadSchedules = useAppStore((s) => s.loadSchedules)
32
35
  const agents = useAppStore((s) => s.agents)
33
36
 
34
37
  const handleClick = () => {
@@ -36,8 +39,22 @@ export function ScheduleCard({ schedule }: Props) {
36
39
  setScheduleSheetOpen(true)
37
40
  }
38
41
 
42
+ const handleToggle = async (e: React.MouseEvent) => {
43
+ e.stopPropagation()
44
+ const newStatus = schedule.status === 'active' ? 'paused' : 'active'
45
+ await api('PUT', `/schedules/${schedule.id}`, { status: newStatus })
46
+ loadSchedules()
47
+ }
48
+
49
+ const handleDelete = async (e: React.MouseEvent) => {
50
+ e.stopPropagation()
51
+ await api('DELETE', `/schedules/${schedule.id}`)
52
+ loadSchedules()
53
+ }
54
+
39
55
  const agent = agents[schedule.agentId]
40
56
  const statusClass = STATUS_COLORS[schedule.status] || STATUS_COLORS.paused
57
+ const canToggle = schedule.status === 'active' || schedule.status === 'paused'
41
58
 
42
59
  return (
43
60
  <div
@@ -48,12 +65,45 @@ export function ScheduleCard({ schedule }: Props) {
48
65
  >
49
66
  <div className="flex items-center gap-2.5">
50
67
  <span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{schedule.name}</span>
51
- <span className={`shrink-0 text-[10px] font-600 uppercase tracking-wider px-2 py-0.5 rounded-[6px] ${statusClass}`}>
52
- {schedule.status}
53
- </span>
68
+ <div className="flex items-center gap-2 shrink-0">
69
+ {!inSidebar && canToggle && (
70
+ <div
71
+ onClick={handleToggle}
72
+ className={`w-9 h-5 rounded-full transition-all relative cursor-pointer shrink-0
73
+ ${schedule.status === 'active' ? 'bg-[#6366F1]' : 'bg-white/[0.08]'}`}
74
+ >
75
+ <div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all
76
+ ${schedule.status === 'active' ? 'left-[18px]' : 'left-0.5'}`} />
77
+ </div>
78
+ )}
79
+ <span className={`text-[10px] font-600 uppercase tracking-wider px-2 py-0.5 rounded-[6px] ${statusClass}`}>
80
+ {schedule.status}
81
+ </span>
82
+ {!inSidebar && (
83
+ <button
84
+ onClick={handleDelete}
85
+ className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
86
+ title="Delete"
87
+ >
88
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
89
+ <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" />
90
+ </svg>
91
+ </button>
92
+ )}
93
+ </div>
54
94
  </div>
55
95
  <div className="text-[12px] text-text-3/70 mt-1.5 truncate">
56
96
  {agent?.name || 'Unknown agent'} &middot; {schedule.scheduleType}
97
+ {!inSidebar && schedule.scheduleType === 'cron' && schedule.cron && (
98
+ <span className="font-mono text-text-3/50 ml-1">({schedule.cron})</span>
99
+ )}
100
+ {!inSidebar && schedule.scheduleType === 'interval' && schedule.intervalMs && (
101
+ <span className="text-text-3/50 ml-1">
102
+ (every {schedule.intervalMs >= 3600000
103
+ ? `${Math.round(schedule.intervalMs / 3600000)}h`
104
+ : `${Math.round(schedule.intervalMs / 60000)}m`})
105
+ </span>
106
+ )}
57
107
  </div>
58
108
  <div className="text-[11px] text-text-3/60 mt-1">
59
109
  Next: {formatNext(schedule.nextRunAt)}
@@ -12,6 +12,7 @@ export function ScheduleList({ inSidebar }: Props) {
12
12
  const schedules = useAppStore((s) => s.schedules)
13
13
  const loadSchedules = useAppStore((s) => s.loadSchedules)
14
14
  const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
15
+ const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
15
16
  const [search, setSearch] = useState('')
16
17
 
17
18
  useEffect(() => { loadSchedules() }, [])
@@ -20,10 +21,11 @@ export function ScheduleList({ inSidebar }: Props) {
20
21
  return Object.values(schedules)
21
22
  .filter((s) => {
22
23
  if (search && !s.name.toLowerCase().includes(search.toLowerCase())) return false
24
+ if (activeProjectFilter && s.projectId !== activeProjectFilter) return false
23
25
  return true
24
26
  })
25
27
  .sort((a, b) => b.createdAt - a.createdAt)
26
- }, [schedules, search])
28
+ }, [schedules, search, activeProjectFilter])
27
29
 
28
30
  if (!filtered.length && !search) {
29
31
  return (
@@ -54,7 +56,7 @@ export function ScheduleList({ inSidebar }: Props) {
54
56
  return (
55
57
  <div className="flex-1 overflow-y-auto">
56
58
  {(filtered.length > 3 || search) && (
57
- <div className="px-4 py-2.5">
59
+ <div className={inSidebar ? 'px-4 py-2.5' : 'px-5 py-2.5'}>
58
60
  <input
59
61
  type="text"
60
62
  value={search}
@@ -66,9 +68,12 @@ export function ScheduleList({ inSidebar }: Props) {
66
68
  />
67
69
  </div>
68
70
  )}
69
- <div className="flex flex-col gap-1 px-2 pb-4">
71
+ <div className={inSidebar
72
+ ? 'flex flex-col gap-1 px-2 pb-4'
73
+ : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3 px-5 pb-6'
74
+ }>
70
75
  {filtered.map((s) => (
71
- <ScheduleCard key={s.id} schedule={s} />
76
+ <ScheduleCard key={s.id} schedule={s} inSidebar={inSidebar} />
72
77
  ))}
73
78
  </div>
74
79
  </div>
@@ -3,9 +3,7 @@
3
3
  import { useEffect, useState, useMemo } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { createSchedule, updateSchedule, deleteSchedule } from '@/lib/schedules'
6
- 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 type { ScheduleType, ScheduleStatus } from '@/types'
10
8
  import cronstrue from 'cronstrue'
11
9
 
@@ -63,49 +61,12 @@ export function ScheduleSheet() {
63
61
  const [status, setStatus] = useState<ScheduleStatus>('active')
64
62
  const [customCron, setCustomCron] = useState(false)
65
63
 
66
- // AI generation state
67
- const [aiPrompt, setAiPrompt] = useState('')
68
- const [generating, setGenerating] = useState(false)
69
- const [generated, setGenerated] = useState(false)
70
- const [genError, setGenError] = useState('')
71
- const appSettings = useAppStore((s) => s.appSettings)
72
- const loadSettings = useAppStore((s) => s.loadSettings)
73
-
74
64
  const editing = editingId ? schedules[editingId] : null
75
65
  const agentList = Object.values(agents)
76
66
 
77
- const handleGenerate = async () => {
78
- if (!aiPrompt.trim()) return
79
- setGenerating(true)
80
- setGenError('')
81
- try {
82
- const result = await api<{ name?: string; taskPrompt?: string; scheduleType?: ScheduleType; cron?: string; intervalMs?: number; error?: string }>('POST', '/generate', { type: 'schedule', prompt: aiPrompt })
83
- if (result.error) {
84
- setGenError(result.error)
85
- } else if (result.name || result.taskPrompt) {
86
- if (result.name) setName(result.name)
87
- if (result.taskPrompt) setTaskPrompt(result.taskPrompt)
88
- if (result.scheduleType) setScheduleType(result.scheduleType)
89
- if (result.cron) { setCron(result.cron); setCustomCron(true) }
90
- if (result.intervalMs) setIntervalMs(result.intervalMs)
91
- setGenerated(true)
92
- } else {
93
- setGenError('AI returned empty response — try again')
94
- }
95
- } catch (err: unknown) {
96
- setGenError(err instanceof Error ? err.message : 'Generation failed')
97
- }
98
- setGenerating(false)
99
- }
100
-
101
67
  useEffect(() => {
102
68
  if (open) {
103
69
  loadAgents()
104
- loadSettings()
105
- setAiPrompt('')
106
- setGenerating(false)
107
- setGenerated(false)
108
- setGenError('')
109
70
  if (editing) {
110
71
  setName(editing.name || '')
111
72
  setAgentId(editing.agentId)
@@ -175,14 +136,6 @@ export function ScheduleSheet() {
175
136
  <p className="text-[14px] text-text-3">Automate agent tasks on a schedule</p>
176
137
  </div>
177
138
 
178
- {/* AI Generation */}
179
- {!editing && <AiGenBlock
180
- aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
181
- generating={generating} generated={generated} genError={genError}
182
- onGenerate={handleGenerate} appSettings={appSettings}
183
- placeholder='Describe the schedule, e.g. "Run keyword research every Monday at 9am"'
184
- />}
185
-
186
139
  <div className="mb-8">
187
140
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
188
141
  <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Daily keyword research" className={inputClass} style={{ fontFamily: 'inherit' }} />
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { useEffect } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
+ import { api } from '@/lib/api-client'
5
6
 
6
7
  interface Props {
7
8
  inSidebar?: boolean
@@ -18,6 +19,12 @@ export function SecretsList({ inSidebar }: Props) {
18
19
  loadSecrets()
19
20
  }, [])
20
21
 
22
+ const handleDelete = async (e: React.MouseEvent, id: string) => {
23
+ e.stopPropagation()
24
+ await api('DELETE', `/secrets/${id}`)
25
+ loadSecrets()
26
+ }
27
+
21
28
  const secretList = Object.values(secrets)
22
29
 
23
30
  if (!secretList.length) {
@@ -44,7 +51,7 @@ export function SecretsList({ inSidebar }: Props) {
44
51
 
45
52
  return (
46
53
  <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
47
- <div className="space-y-2">
54
+ <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
48
55
  {secretList.map((secret) => {
49
56
  const scopeLabel = secret.scope === 'global'
50
57
  ? 'All orchestrators'
@@ -68,7 +75,18 @@ export function SecretsList({ inSidebar }: Props) {
68
75
  <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
69
76
  <path d="M7 11V7a5 5 0 0 1 10 0v4" />
70
77
  </svg>
71
- <span className="text-[14px] font-600 text-text truncate">{secret.name}</span>
78
+ <span className="text-[14px] font-600 text-text truncate flex-1">{secret.name}</span>
79
+ {!inSidebar && (
80
+ <button
81
+ onClick={(e) => handleDelete(e, secret.id)}
82
+ className="text-text-3/40 hover:text-red-400 transition-colors p-0.5 shrink-0"
83
+ title="Delete"
84
+ >
85
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
86
+ <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" />
87
+ </svg>
88
+ </button>
89
+ )}
72
90
  </div>
73
91
  <div className="flex items-center gap-2 pl-[22px]">
74
92
  <span className="text-[11px] font-mono text-text-3">{secret.service}</span>
@@ -7,6 +7,7 @@ import { createSession, createCredential } from '@/lib/sessions'
7
7
  import { BottomSheet } from '@/components/shared/bottom-sheet'
8
8
  import { DirBrowser } from '@/components/shared/dir-browser'
9
9
  import { TOOL_LABELS, TOOL_DESCRIPTIONS } from '@/components/chat/tool-call-bubble'
10
+ import { ModelCombobox } from '@/components/shared/model-combobox'
10
11
  import type { ProviderType, SessionTool } from '@/types'
11
12
 
12
13
  export function NewSessionSheet() {
@@ -128,7 +129,7 @@ export function NewSessionSheet() {
128
129
  }
129
130
 
130
131
  const handleCreate = async () => {
131
- const sessionName = name.trim() || 'New Session'
132
+ const sessionName = name.trim() || 'New Chat'
132
133
  const cwd = selectedDir || ''
133
134
  const resolvedCredentialId = currentProvider?.requiresApiKey
134
135
  ? credentialId
@@ -173,14 +174,14 @@ export function NewSessionSheet() {
173
174
  <BottomSheet open={open} onClose={onClose} wide>
174
175
  {/* Header */}
175
176
  <div className="mb-10">
176
- <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">New Session</h2>
177
- <p className="text-[14px] text-text-3">Configure your AI session</p>
177
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">New Chat</h2>
178
+ <p className="text-[14px] text-text-3">Configure your AI chat</p>
178
179
  </div>
179
180
 
180
181
  {/* Name */}
181
182
  <div className="mb-8">
182
183
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
183
- Session Name
184
+ Chat Name
184
185
  </label>
185
186
  <input
186
187
  type="text"
@@ -269,16 +270,14 @@ export function NewSessionSheet() {
269
270
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
270
271
  Model
271
272
  </label>
272
- <select
273
+ <ModelCombobox
274
+ providerId={currentProvider.id}
273
275
  value={model}
274
- onChange={(e) => setModel(e.target.value)}
275
- className={`${inputClass} appearance-none cursor-pointer`}
276
- style={{ fontFamily: 'inherit' }}
277
- >
278
- {currentProvider.models.map((m) => (
279
- <option key={m} value={m}>{m}</option>
280
- ))}
281
- </select>
276
+ onChange={setModel}
277
+ models={currentProvider.models}
278
+ defaultModels={currentProvider.defaultModels}
279
+ className={`${inputClass} cursor-pointer`}
280
+ />
282
281
  </div>
283
282
  )}
284
283
 
@@ -374,7 +373,7 @@ export function NewSessionSheet() {
374
373
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
375
374
  Tools <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
376
375
  </label>
377
- <p className="text-[12px] text-text-3/60 mb-3">Allow this model to execute commands and access files in the session directory.</p>
376
+ <p className="text-[12px] text-text-3/60 mb-3">Allow this model to execute commands and access files in the working directory.</p>
378
377
  <div className="flex flex-wrap gap-2.5">
379
378
  {([
380
379
  { id: 'shell' as SessionTool, label: 'Shell' },
@@ -470,7 +469,7 @@ export function NewSessionSheet() {
470
469
  shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
471
470
  style={{ fontFamily: 'inherit' }}
472
471
  >
473
- Create Session
472
+ Create Chat
474
473
  </button>
475
474
  </div>
476
475
  </BottomSheet>
@@ -117,7 +117,7 @@ export function SessionCard({ session, active, onClick }: Props) {
117
117
  onClick={handleDelete}
118
118
  className="shrink-0 opacity-0 group-hover/card:opacity-100 transition-opacity duration-150
119
119
  text-text-3 hover:text-red-400 p-0.5 -mr-1 cursor-pointer bg-transparent border-none"
120
- title="Delete session"
120
+ title="Delete chat"
121
121
  >
122
122
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
123
123
  <line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
@@ -89,7 +89,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
89
89
  <path d="M12 2L14.5 9.5L22 12L14.5 14.5L12 22L9.5 14.5L2 12L9.5 9.5L12 2Z" fill="currentColor" />
90
90
  </svg>
91
91
  </div>
92
- <p className="font-display text-[15px] font-600 text-text-2">No sessions yet</p>
92
+ <p className="font-display text-[15px] font-600 text-text-2">No chats yet</p>
93
93
  <p className="text-[13px] text-text-3/50">Create one to start chatting</p>
94
94
  {!inSidebar && (
95
95
  <button
@@ -99,7 +99,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
99
99
  shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
100
100
  style={{ fontFamily: 'inherit' }}
101
101
  >
102
- + New Session
102
+ + New Chat
103
103
  </button>
104
104
  )}
105
105
  </div>
@@ -124,12 +124,12 @@ export function SessionList({ inSidebar, onSelect }: Props) {
124
124
  {filtered.length > 0 && (
125
125
  <button
126
126
  onClick={async () => {
127
- if (!window.confirm(`Delete ${filtered.length} session${filtered.length === 1 ? '' : 's'}?`)) return
127
+ if (!window.confirm(`Delete ${filtered.length} chat${filtered.length === 1 ? '' : 's'}?`)) return
128
128
  await clearSessions(filtered.map((s) => s.id))
129
129
  }}
130
130
  className="ml-auto p-1.5 rounded-[8px] text-text-3/70 hover:text-red-400 hover:bg-red-400/[0.06]
131
131
  cursor-pointer transition-all bg-transparent border-none"
132
- title="Clear all sessions"
132
+ title="Clear all chats"
133
133
  >
134
134
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
135
135
  <polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
@@ -154,7 +154,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
154
154
  <select
155
155
  value={sortMode}
156
156
  onChange={(e) => setSortMode(e.target.value as SortMode)}
157
- aria-label="Sort sessions"
157
+ aria-label="Sort chats"
158
158
  className="px-2 py-2 rounded-[12px] border border-white/[0.04] bg-surface text-text
159
159
  text-[11px] outline-none cursor-pointer"
160
160
  style={{ fontFamily: 'inherit' }}
@@ -176,7 +176,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
176
176
  />
177
177
  <button
178
178
  onClick={(e) => { e.stopPropagation(); togglePinSession(s.id) }}
179
- aria-label={s.pinned ? 'Unpin session' : 'Pin session'}
179
+ aria-label={s.pinned ? 'Unpin chat' : 'Pin chat'}
180
180
  className={`absolute top-2 right-2 p-1 rounded-[6px] border-none cursor-pointer transition-all
181
181
  ${s.pinned
182
182
  ? 'text-amber-400 bg-amber-400/10 opacity-100'
@@ -193,7 +193,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
193
193
  ) : (
194
194
  <div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
195
195
  <p className="text-[13px] text-text-3/50">
196
- No {typeFilter === 'orchestrated' ? 'AI' : typeFilter === 'active' ? 'active' : typeFilter} sessions{search ? ` matching "${search}"` : ''}
196
+ No {typeFilter === 'orchestrated' ? 'AI' : typeFilter === 'active' ? 'active' : typeFilter} chats{search ? ` matching "${search}"` : ''}
197
197
  </p>
198
198
  </div>
199
199
  )}