@swarmclawai/swarmclaw 0.4.0 → 0.5.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 (209) hide show
  1. package/README.md +21 -4
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +13 -0
  4. package/package.json +3 -1
  5. package/src/app/api/agents/[id]/route.ts +39 -22
  6. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  7. package/src/app/api/agents/route.ts +3 -2
  8. package/src/app/api/agents/trash/route.ts +44 -0
  9. package/src/app/api/clawhub/install/route.ts +2 -2
  10. package/src/app/api/connectors/[id]/route.ts +17 -7
  11. package/src/app/api/connectors/[id]/webhook/route.ts +103 -0
  12. package/src/app/api/connectors/route.ts +6 -3
  13. package/src/app/api/credentials/[id]/route.ts +2 -1
  14. package/src/app/api/credentials/route.ts +2 -2
  15. package/src/app/api/documents/route.ts +2 -2
  16. package/src/app/api/files/serve/route.ts +8 -0
  17. package/src/app/api/knowledge/[id]/route.ts +5 -4
  18. package/src/app/api/knowledge/upload/route.ts +2 -2
  19. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  20. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  21. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  22. package/src/app/api/mcp-servers/route.ts +2 -2
  23. package/src/app/api/memory/[id]/route.ts +9 -8
  24. package/src/app/api/memory/route.ts +2 -2
  25. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  26. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  27. package/src/app/api/openclaw/approvals/route.ts +46 -0
  28. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  29. package/src/app/api/openclaw/cron/route.ts +52 -0
  30. package/src/app/api/openclaw/directory/route.ts +27 -0
  31. package/src/app/api/openclaw/discover/route.ts +62 -0
  32. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  33. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  34. package/src/app/api/openclaw/gateway/route.ts +72 -0
  35. package/src/app/api/openclaw/history/route.ts +109 -0
  36. package/src/app/api/openclaw/media/route.ts +53 -0
  37. package/src/app/api/openclaw/models/route.ts +12 -0
  38. package/src/app/api/openclaw/permissions/route.ts +39 -0
  39. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  40. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  41. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  42. package/src/app/api/openclaw/skills/route.ts +82 -0
  43. package/src/app/api/openclaw/sync/route.ts +31 -0
  44. package/src/app/api/orchestrator/run/route.ts +2 -2
  45. package/src/app/api/projects/[id]/route.ts +55 -0
  46. package/src/app/api/projects/route.ts +27 -0
  47. package/src/app/api/providers/[id]/models/route.ts +2 -1
  48. package/src/app/api/providers/[id]/route.ts +13 -15
  49. package/src/app/api/providers/route.ts +2 -2
  50. package/src/app/api/schedules/[id]/route.ts +16 -18
  51. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  52. package/src/app/api/schedules/route.ts +2 -2
  53. package/src/app/api/secrets/[id]/route.ts +16 -17
  54. package/src/app/api/secrets/route.ts +2 -2
  55. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  56. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  57. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  58. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  59. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  60. package/src/app/api/sessions/[id]/messages/route.ts +20 -2
  61. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  62. package/src/app/api/sessions/[id]/route.ts +14 -4
  63. package/src/app/api/sessions/route.ts +8 -4
  64. package/src/app/api/skills/[id]/route.ts +23 -21
  65. package/src/app/api/skills/import/route.ts +2 -2
  66. package/src/app/api/skills/route.ts +2 -2
  67. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  68. package/src/app/api/tasks/[id]/route.ts +6 -5
  69. package/src/app/api/tasks/route.ts +2 -2
  70. package/src/app/api/tts/stream/route.ts +48 -0
  71. package/src/app/api/upload/route.ts +2 -2
  72. package/src/app/api/uploads/[filename]/route.ts +4 -1
  73. package/src/app/api/webhooks/[id]/route.ts +29 -31
  74. package/src/app/api/webhooks/route.ts +2 -2
  75. package/src/app/globals.css +14 -0
  76. package/src/app/layout.tsx +5 -20
  77. package/src/app/page.tsx +3 -24
  78. package/src/cli/index.js +60 -0
  79. package/src/cli/index.ts +1 -1
  80. package/src/cli/spec.js +42 -0
  81. package/src/components/agents/agent-avatar.tsx +45 -0
  82. package/src/components/agents/agent-card.tsx +19 -5
  83. package/src/components/agents/agent-chat-list.tsx +31 -24
  84. package/src/components/agents/agent-files-editor.tsx +185 -0
  85. package/src/components/agents/agent-list.tsx +84 -3
  86. package/src/components/agents/agent-sheet.tsx +147 -14
  87. package/src/components/agents/cron-job-form.tsx +137 -0
  88. package/src/components/agents/exec-config-panel.tsx +147 -0
  89. package/src/components/agents/inspector-panel.tsx +310 -0
  90. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  91. package/src/components/agents/permission-preset-selector.tsx +79 -0
  92. package/src/components/agents/personality-builder.tsx +111 -0
  93. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  94. package/src/components/agents/skill-install-dialog.tsx +102 -0
  95. package/src/components/agents/trash-list.tsx +109 -0
  96. package/src/components/chat/chat-area.tsx +41 -6
  97. package/src/components/chat/chat-header.tsx +305 -29
  98. package/src/components/chat/chat-preview-panel.tsx +113 -0
  99. package/src/components/chat/exec-approval-card.tsx +89 -0
  100. package/src/components/chat/message-bubble.tsx +218 -36
  101. package/src/components/chat/message-list.tsx +135 -31
  102. package/src/components/chat/streaming-bubble.tsx +59 -10
  103. package/src/components/chat/suggestions-bar.tsx +74 -0
  104. package/src/components/chat/thinking-indicator.tsx +20 -6
  105. package/src/components/chat/tool-call-bubble.tsx +98 -19
  106. package/src/components/chat/tool-request-banner.tsx +20 -2
  107. package/src/components/chat/trace-block.tsx +103 -0
  108. package/src/components/chat/voice-overlay.tsx +80 -0
  109. package/src/components/connectors/connector-list.tsx +6 -2
  110. package/src/components/connectors/connector-sheet.tsx +31 -7
  111. package/src/components/layout/app-layout.tsx +47 -25
  112. package/src/components/projects/project-list.tsx +123 -0
  113. package/src/components/projects/project-sheet.tsx +135 -0
  114. package/src/components/schedules/schedule-list.tsx +3 -1
  115. package/src/components/sessions/new-session-sheet.tsx +6 -6
  116. package/src/components/sessions/session-card.tsx +1 -1
  117. package/src/components/sessions/session-list.tsx +7 -7
  118. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  119. package/src/components/shared/avatar.tsx +13 -2
  120. package/src/components/shared/connector-platform-icon.tsx +4 -0
  121. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  122. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  123. package/src/components/shared/settings/section-web-search.tsx +56 -0
  124. package/src/components/shared/settings/settings-page.tsx +74 -0
  125. package/src/components/skills/skill-list.tsx +2 -1
  126. package/src/components/tasks/task-board.tsx +1 -1
  127. package/src/components/tasks/task-list.tsx +5 -2
  128. package/src/components/tasks/task-sheet.tsx +12 -12
  129. package/src/hooks/use-continuous-speech.ts +181 -0
  130. package/src/hooks/use-openclaw-gateway.ts +63 -0
  131. package/src/hooks/use-view-router.ts +52 -0
  132. package/src/hooks/use-voice-conversation.ts +80 -0
  133. package/src/lib/id.ts +6 -0
  134. package/src/lib/notification-sounds.ts +58 -0
  135. package/src/lib/personality-parser.ts +97 -0
  136. package/src/lib/projects.ts +13 -0
  137. package/src/lib/provider-sets.ts +5 -0
  138. package/src/lib/providers/anthropic.ts +14 -1
  139. package/src/lib/providers/index.ts +6 -0
  140. package/src/lib/providers/ollama.ts +9 -1
  141. package/src/lib/providers/openai.ts +9 -1
  142. package/src/lib/providers/openclaw.ts +28 -2
  143. package/src/lib/runtime-loop.ts +2 -2
  144. package/src/lib/server/api-routes.test.ts +5 -6
  145. package/src/lib/server/build-llm.ts +17 -4
  146. package/src/lib/server/chat-execution.ts +82 -6
  147. package/src/lib/server/collection-helpers.ts +54 -0
  148. package/src/lib/server/connectors/bluebubbles.test.ts +217 -0
  149. package/src/lib/server/connectors/bluebubbles.ts +360 -0
  150. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  151. package/src/lib/server/connectors/googlechat.ts +51 -8
  152. package/src/lib/server/connectors/manager.ts +424 -13
  153. package/src/lib/server/connectors/media.ts +2 -2
  154. package/src/lib/server/connectors/openclaw.ts +65 -0
  155. package/src/lib/server/connectors/pairing.test.ts +99 -0
  156. package/src/lib/server/connectors/pairing.ts +256 -0
  157. package/src/lib/server/connectors/signal.ts +1 -0
  158. package/src/lib/server/connectors/teams.ts +5 -5
  159. package/src/lib/server/connectors/types.ts +10 -0
  160. package/src/lib/server/daemon-state.ts +11 -0
  161. package/src/lib/server/execution-log.ts +3 -3
  162. package/src/lib/server/heartbeat-service.ts +1 -1
  163. package/src/lib/server/knowledge-db.test.ts +2 -33
  164. package/src/lib/server/main-agent-loop.ts +8 -9
  165. package/src/lib/server/main-session.ts +21 -0
  166. package/src/lib/server/memory-db.ts +6 -6
  167. package/src/lib/server/openclaw-approvals.ts +105 -0
  168. package/src/lib/server/openclaw-config-sync.ts +107 -0
  169. package/src/lib/server/openclaw-exec-config.ts +52 -0
  170. package/src/lib/server/openclaw-gateway.ts +291 -0
  171. package/src/lib/server/openclaw-history-merge.ts +36 -0
  172. package/src/lib/server/openclaw-models.ts +56 -0
  173. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  174. package/src/lib/server/openclaw-sync.ts +497 -0
  175. package/src/lib/server/orchestrator-lg.ts +30 -9
  176. package/src/lib/server/orchestrator.ts +4 -4
  177. package/src/lib/server/process-manager.ts +2 -2
  178. package/src/lib/server/queue.ts +24 -11
  179. package/src/lib/server/scheduler.ts +2 -2
  180. package/src/lib/server/session-mailbox.ts +2 -2
  181. package/src/lib/server/session-run-manager.ts +2 -2
  182. package/src/lib/server/session-tools/connector.ts +53 -6
  183. package/src/lib/server/session-tools/crud.ts +3 -3
  184. package/src/lib/server/session-tools/delegate.ts +22 -6
  185. package/src/lib/server/session-tools/file.ts +192 -19
  186. package/src/lib/server/session-tools/index.ts +4 -2
  187. package/src/lib/server/session-tools/memory.ts +2 -2
  188. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  189. package/src/lib/server/session-tools/sandbox.ts +33 -0
  190. package/src/lib/server/session-tools/search-providers.ts +277 -0
  191. package/src/lib/server/session-tools/session-info.ts +2 -2
  192. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  193. package/src/lib/server/session-tools/shell.ts +1 -1
  194. package/src/lib/server/session-tools/web.ts +53 -72
  195. package/src/lib/server/storage.ts +74 -11
  196. package/src/lib/server/stream-agent-chat.ts +53 -4
  197. package/src/lib/server/suggestions.ts +20 -0
  198. package/src/lib/server/task-result.test.ts +44 -0
  199. package/src/lib/server/task-result.ts +14 -0
  200. package/src/lib/server/ws-hub.ts +14 -0
  201. package/src/lib/tool-definitions.ts +5 -3
  202. package/src/lib/tts-stream.ts +130 -0
  203. package/src/lib/view-routes.ts +28 -0
  204. package/src/proxy.ts +3 -0
  205. package/src/stores/use-app-store.ts +80 -1
  206. package/src/stores/use-approval-store.ts +78 -0
  207. package/src/stores/use-chat-store.ts +162 -6
  208. package/src/types/index.ts +154 -3
  209. package/tsconfig.json +13 -4
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
5
5
  import { useChatStore } from '@/stores/use-chat-store'
6
6
  import { fetchMessages } from '@/lib/sessions'
7
7
  import type { Agent, Session } from '@/types'
8
+ import { AgentAvatar } from './agent-avatar'
8
9
 
9
10
  interface Props {
10
11
  inSidebar?: boolean
@@ -130,32 +131,38 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
130
131
  style={{ fontFamily: 'inherit' }}
131
132
  >
132
133
  <div className="flex items-center gap-2.5">
133
- {/* Status dot */}
134
- <div className={`w-2 h-2 rounded-full shrink-0 ${
135
- isWorking ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]' : 'bg-text-3/20'
136
- }`} />
137
- <span className="font-display text-[13.5px] font-600 truncate flex-1 tracking-[-0.01em]">
138
- {agent.name}
139
- </span>
140
- {/* Provider badge */}
141
- <span className="text-[10px] text-text-3/60 font-mono shrink-0">
142
- {agent.model ? agent.model.split('/').pop()?.split(':')[0] : agent.provider}
143
- </span>
144
- </div>
145
- {isTyping ? (
146
- <div className="text-[12px] text-accent-bright/70 mt-1 pl-[18px] flex items-center gap-1.5">
147
- <span className="flex gap-0.5">
148
- <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:0ms]" />
149
- <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:150ms]" />
150
- <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:300ms]" />
151
- </span>
152
- Typing...
134
+ {/* Avatar with status dot */}
135
+ <div className="relative shrink-0">
136
+ <AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={36} />
137
+ <div className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-bg ${
138
+ isWorking ? 'bg-emerald-400 shadow-[0_0_6px_rgba(52,211,153,0.4)]' : 'bg-text-3/30'
139
+ }`} />
153
140
  </div>
154
- ) : preview ? (
155
- <div className="text-[12px] text-text-3/70 mt-1 truncate pl-[18px]">
156
- {preview}
141
+ <div className="flex flex-col flex-1 min-w-0">
142
+ <div className="flex items-center gap-2">
143
+ <span className="font-display text-[13.5px] font-600 truncate flex-1 tracking-[-0.01em]">
144
+ {agent.name}
145
+ </span>
146
+ <span className="text-[10px] text-text-3/60 font-mono shrink-0">
147
+ {agent.model ? agent.model.split('/').pop()?.split(':')[0] : agent.provider}
148
+ </span>
149
+ </div>
150
+ {isTyping ? (
151
+ <div className="text-[12px] text-accent-bright/70 mt-0.5 flex items-center gap-1.5">
152
+ <span className="flex gap-0.5">
153
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:0ms]" />
154
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:150ms]" />
155
+ <span className="w-1 h-1 rounded-full bg-accent-bright/70 animate-bounce [animation-delay:300ms]" />
156
+ </span>
157
+ Typing...
158
+ </div>
159
+ ) : preview ? (
160
+ <div className="text-[12px] text-text-3/70 mt-0.5 truncate">
161
+ {preview}
162
+ </div>
163
+ ) : null}
157
164
  </div>
158
- ) : null}
165
+ </div>
159
166
  </button>
160
167
  )
161
168
  })}
@@ -0,0 +1,185 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useState } from 'react'
4
+ import { api } from '@/lib/api-client'
5
+ import { PersonalityBuilder } from './personality-builder'
6
+
7
+ const FILES = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md', 'MEMORY.md', 'AGENTS.md'] as const
8
+ const GUIDED_FILES = new Set(['SOUL.md', 'IDENTITY.md', 'USER.md'])
9
+
10
+ interface FileState {
11
+ content: string
12
+ original: string
13
+ loading: boolean
14
+ saving: boolean
15
+ error?: string
16
+ }
17
+
18
+ interface Props {
19
+ agentId: string
20
+ }
21
+
22
+ export function AgentFilesEditor({ agentId }: Props) {
23
+ const [activeTab, setActiveTab] = useState<string>(FILES[0])
24
+ const [files, setFiles] = useState<Record<string, FileState>>({})
25
+ const [guidedMode, setGuidedMode] = useState(false)
26
+
27
+ const loadFiles = useCallback(async () => {
28
+ const initial: Record<string, FileState> = {}
29
+ for (const f of FILES) {
30
+ initial[f] = { content: '', original: '', loading: true, saving: false }
31
+ }
32
+ setFiles(initial)
33
+
34
+ try {
35
+ const result = await api<Record<string, { content: string; error?: string }>>('GET', `/openclaw/agent-files?agentId=${agentId}`)
36
+ setFiles((prev) => {
37
+ const next = { ...prev }
38
+ for (const [name, data] of Object.entries(result)) {
39
+ next[name] = {
40
+ content: data.content,
41
+ original: data.content,
42
+ loading: false,
43
+ saving: false,
44
+ error: data.error,
45
+ }
46
+ }
47
+ return next
48
+ })
49
+ } catch (err: unknown) {
50
+ const message = err instanceof Error ? err.message : String(err)
51
+ setFiles((prev) => {
52
+ const next = { ...prev }
53
+ for (const f of FILES) {
54
+ next[f] = { ...next[f], loading: false, error: message }
55
+ }
56
+ return next
57
+ })
58
+ }
59
+ }, [agentId])
60
+
61
+ useEffect(() => { loadFiles() }, [loadFiles])
62
+
63
+ const handleContentChange = (filename: string, content: string) => {
64
+ setFiles((prev) => ({
65
+ ...prev,
66
+ [filename]: { ...prev[filename], content },
67
+ }))
68
+ }
69
+
70
+ const handleSave = async (filename: string) => {
71
+ const file = files[filename]
72
+ if (!file || file.content === file.original) return
73
+
74
+ setFiles((prev) => ({
75
+ ...prev,
76
+ [filename]: { ...prev[filename], saving: true, error: undefined },
77
+ }))
78
+
79
+ try {
80
+ await api('PUT', '/openclaw/agent-files', { agentId, filename, content: file.content })
81
+ setFiles((prev) => ({
82
+ ...prev,
83
+ [filename]: { ...prev[filename], saving: false, original: prev[filename].content },
84
+ }))
85
+ } catch (err: unknown) {
86
+ const message = err instanceof Error ? err.message : String(err)
87
+ setFiles((prev) => ({
88
+ ...prev,
89
+ [filename]: { ...prev[filename], saving: false, error: message },
90
+ }))
91
+ }
92
+ }
93
+
94
+ const handleGuidedSave = (content: string) => {
95
+ handleContentChange(activeTab, content)
96
+ }
97
+
98
+ const current = files[activeTab]
99
+ const isDirty = current && current.content !== current.original
100
+ const showGuided = guidedMode && GUIDED_FILES.has(activeTab)
101
+
102
+ return (
103
+ <div className="flex flex-col h-full">
104
+ {/* Tab bar */}
105
+ <div className="flex gap-0.5 px-2 pt-2 pb-1 overflow-x-auto shrink-0">
106
+ {FILES.map((f) => {
107
+ const fileState = files[f]
108
+ const dirty = fileState && fileState.content !== fileState.original
109
+ return (
110
+ <button
111
+ key={f}
112
+ onClick={() => setActiveTab(f)}
113
+ className={`px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all whitespace-nowrap
114
+ ${activeTab === f
115
+ ? 'bg-accent-soft text-accent-bright'
116
+ : 'bg-transparent text-text-3 hover:text-text-2'}`}
117
+ style={{ fontFamily: 'inherit' }}
118
+ >
119
+ {f.replace('.md', '')}
120
+ {dirty && <span className="ml-1 text-amber-400">*</span>}
121
+ </button>
122
+ )
123
+ })}
124
+ </div>
125
+
126
+ {/* Guided toggle for personality files */}
127
+ {GUIDED_FILES.has(activeTab) && (
128
+ <div className="px-3 py-1 shrink-0">
129
+ <button
130
+ onClick={() => setGuidedMode(!guidedMode)}
131
+ className={`text-[10px] font-600 px-2 py-0.5 rounded-[6px] cursor-pointer transition-all border-none
132
+ ${guidedMode ? 'bg-accent-soft text-accent-bright' : 'bg-white/[0.04] text-text-3 hover:text-text-2'}`}
133
+ style={{ fontFamily: 'inherit' }}
134
+ >
135
+ {guidedMode ? 'Raw Editor' : 'Guided Editor'}
136
+ </button>
137
+ </div>
138
+ )}
139
+
140
+ {/* Editor area */}
141
+ <div className="flex-1 min-h-0 px-2 pb-2 overflow-y-auto">
142
+ {current?.loading ? (
143
+ <div className="flex items-center justify-center h-full text-[13px] text-text-3/50">Loading...</div>
144
+ ) : current?.error ? (
145
+ <div className="flex items-center justify-center h-full text-[13px] text-red-400">{current.error}</div>
146
+ ) : showGuided ? (
147
+ <div className="p-2">
148
+ <PersonalityBuilder
149
+ agentId={agentId}
150
+ fileType={activeTab as 'IDENTITY.md' | 'USER.md' | 'SOUL.md'}
151
+ content={current?.content ?? ''}
152
+ onSave={handleGuidedSave}
153
+ />
154
+ </div>
155
+ ) : (
156
+ <textarea
157
+ value={current?.content ?? ''}
158
+ onChange={(e) => handleContentChange(activeTab, e.target.value)}
159
+ className="w-full h-full resize-none rounded-[10px] border border-white/[0.06] bg-black/20 px-3 py-2.5
160
+ text-[13px] text-text font-mono leading-relaxed outline-none
161
+ placeholder:text-text-3/40 focus:border-white/[0.12] transition-colors"
162
+ placeholder={`${activeTab} content...`}
163
+ style={{ fontFamily: 'ui-monospace, monospace' }}
164
+ />
165
+ )}
166
+ </div>
167
+
168
+ {/* Save bar */}
169
+ <div className="shrink-0 px-3 pb-2 flex items-center gap-2">
170
+ <button
171
+ onClick={() => handleSave(activeTab)}
172
+ disabled={!isDirty || current?.saving}
173
+ className="px-4 py-1.5 rounded-[8px] border-none bg-accent-bright text-white text-[12px] font-600
174
+ cursor-pointer disabled:opacity-30 disabled:cursor-not-allowed transition-all hover:brightness-110"
175
+ style={{ fontFamily: 'inherit' }}
176
+ >
177
+ {current?.saving ? 'Saving...' : 'Save'}
178
+ </button>
179
+ {isDirty && (
180
+ <span className="text-[11px] text-amber-400/70">Unsaved changes</span>
181
+ )}
182
+ </div>
183
+ </div>
184
+ )
185
+ }
@@ -4,6 +4,8 @@ import { useEffect, useMemo, useState, useCallback } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
6
  import { AgentCard } from './agent-card'
7
+ import { TrashList } from './trash-list'
8
+ import { useApprovalStore } from '@/stores/use-approval-store'
7
9
 
8
10
  interface Props {
9
11
  inSidebar?: boolean
@@ -16,6 +18,12 @@ export function AgentList({ inSidebar }: Props) {
16
18
  const currentUser = useAppStore((s) => s.currentUser)
17
19
  const loadSessions = useAppStore((s) => s.loadSessions)
18
20
  const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
21
+ const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
22
+ const showTrash = useAppStore((s) => s.showTrash)
23
+ const setShowTrash = useAppStore((s) => s.setShowTrash)
24
+ const fleetFilter = useAppStore((s) => s.fleetFilter)
25
+ const setFleetFilter = useAppStore((s) => s.setFleetFilter)
26
+ const approvals = useApprovalStore((s) => s.approvals)
19
27
  const [search, setSearch] = useState('')
20
28
  const [filter, setFilter] = useState<'all' | 'orchestrator' | 'agent'>('all')
21
29
 
@@ -35,16 +43,59 @@ export function AgentList({ inSidebar }: Props) {
35
43
 
36
44
  useEffect(() => { loadAgents() }, [])
37
45
 
46
+ // Compute which agents are "running" (have active sessions)
47
+ const runningAgentIds = useMemo(() => {
48
+ const ids = new Set<string>()
49
+ for (const s of Object.values(sessions)) {
50
+ if (s.agentId && s.active) ids.add(s.agentId)
51
+ }
52
+ return ids
53
+ }, [sessions])
54
+
55
+ // Approval counts per agent
56
+ const approvalsByAgent = useMemo(() => {
57
+ const counts: Record<string, number> = {}
58
+ for (const a of Object.values(approvals)) {
59
+ counts[a.agentId] = (counts[a.agentId] || 0) + 1
60
+ }
61
+ return counts
62
+ }, [approvals])
63
+
38
64
  const filtered = useMemo(() => {
39
65
  return Object.values(agents)
40
66
  .filter((p) => {
41
67
  if (search && !p.name.toLowerCase().includes(search.toLowerCase())) return false
42
68
  if (filter === 'orchestrator' && !p.isOrchestrator) return false
43
69
  if (filter === 'agent' && p.isOrchestrator) return false
70
+ if (activeProjectFilter && p.projectId !== activeProjectFilter) return false
71
+ // Fleet filter
72
+ if (fleetFilter === 'running' && !runningAgentIds.has(p.id)) return false
73
+ if (fleetFilter === 'approvals' && !(approvalsByAgent[p.id] > 0)) return false
44
74
  return true
45
75
  })
46
76
  .sort((a, b) => b.updatedAt - a.updatedAt)
47
- }, [agents, search, filter])
77
+ }, [agents, search, filter, activeProjectFilter, fleetFilter, runningAgentIds, approvalsByAgent])
78
+
79
+ if (showTrash) {
80
+ return (
81
+ <div className="flex-1 flex flex-col overflow-hidden">
82
+ <div className="px-4 py-2.5 flex items-center gap-2">
83
+ <button
84
+ onClick={() => setShowTrash(false)}
85
+ className="px-3 py-1.5 rounded-[8px] text-[12px] font-600 text-text-3 bg-transparent border-none cursor-pointer hover:text-text-2 transition-all flex items-center gap-1.5"
86
+ style={{ fontFamily: 'inherit' }}
87
+ >
88
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
89
+ <path d="M19 12H5" /><polyline points="12 19 5 12 12 5" />
90
+ </svg>
91
+ Back to Agents
92
+ </button>
93
+ <span className="text-[13px] font-600 text-text-2">Trash</span>
94
+ </div>
95
+ <TrashList />
96
+ </div>
97
+ )
98
+ }
48
99
 
49
100
  if (!filtered.length && !search) {
50
101
  return (
@@ -87,7 +138,26 @@ export function AgentList({ inSidebar }: Props) {
87
138
  />
88
139
  </div>
89
140
  )}
90
- <div className="flex gap-1 px-4 pb-2">
141
+ {/* Fleet filter: All / Running / Approvals */}
142
+ <div className="flex gap-1 px-4 pb-1 items-center">
143
+ {(['all', 'running', 'approvals'] as const).map((f) => {
144
+ const count = f === 'running' ? runningAgentIds.size
145
+ : f === 'approvals' ? Object.keys(approvalsByAgent).length
146
+ : null
147
+ return (
148
+ <button
149
+ key={f}
150
+ onClick={() => setFleetFilter(f)}
151
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all
152
+ ${fleetFilter === f ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
153
+ style={{ fontFamily: 'inherit' }}
154
+ >
155
+ {f}{count ? ` (${count})` : ''}
156
+ </button>
157
+ )
158
+ })}
159
+ </div>
160
+ <div className="flex gap-1 px-4 pb-2 items-center">
91
161
  {(['all', 'orchestrator', 'agent'] as const).map((f) => (
92
162
  <button
93
163
  key={f}
@@ -99,10 +169,21 @@ export function AgentList({ inSidebar }: Props) {
99
169
  {f}
100
170
  </button>
101
171
  ))}
172
+ <div className="flex-1" />
173
+ <button
174
+ onClick={() => setShowTrash(true)}
175
+ aria-label="View trash"
176
+ className="p-1.5 rounded-[6px] text-text-3/50 hover:text-text-3 bg-transparent border-none cursor-pointer transition-all hover:bg-white/[0.04]"
177
+ >
178
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
179
+ <polyline points="3 6 5 6 21 6" />
180
+ <path d="M19 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" />
181
+ </svg>
182
+ </button>
102
183
  </div>
103
184
  <div className="flex flex-col gap-1 px-2 pb-4">
104
185
  {filtered.map((p) => (
105
- <AgentCard key={p.id} agent={p} isDefault={p.id === defaultAgentId} onSetDefault={handleSetDefault} />
186
+ <AgentCard key={p.id} agent={p} isDefault={p.id === defaultAgentId} isRunning={runningAgentIds.has(p.id)} onSetDefault={handleSetDefault} />
106
187
  ))}
107
188
  </div>
108
189
  </div>
@@ -9,8 +9,43 @@ import { toast } from 'sonner'
9
9
  import { ModelCombobox } from '@/components/shared/model-combobox'
10
10
  import type { ProviderType, ClaudeSkill } from '@/types'
11
11
  import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
12
+ import { NATIVE_CAPABILITY_PROVIDER_IDS, NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
13
+ import { AgentAvatar } from './agent-avatar'
12
14
 
13
- const NATIVE_CAPABILITY_PROVIDER_IDS = new Set<ProviderType>(['claude-cli', 'codex-cli', 'opencode-cli', 'openclaw'])
15
+ const HB_PRESETS = [30, 60, 120, 300, 600, 1800, 3600] as const
16
+
17
+ function formatHbDuration(sec: number): string {
18
+ if (sec >= 3600) {
19
+ const h = Math.floor(sec / 3600)
20
+ const m = Math.floor((sec % 3600) / 60)
21
+ return m > 0 ? `${h}h${m}m` : `${h}h`
22
+ }
23
+ if (sec >= 60) return `${Math.floor(sec / 60)}m`
24
+ return `${sec}s`
25
+ }
26
+
27
+ /** Parse a stored heartbeatInterval string or heartbeatIntervalSec number to a select-friendly string of seconds */
28
+ function parseDurationToSec(interval: string | number | null | undefined, intervalSec: number | null | undefined): string {
29
+ if (intervalSec != null && Number.isFinite(intervalSec) && intervalSec > 0) {
30
+ // Snap to nearest preset if close, otherwise use raw value
31
+ const closest = HB_PRESETS.find((p) => p === Math.round(intervalSec))
32
+ if (closest) return String(closest)
33
+ }
34
+ if (typeof interval === 'number' && Number.isFinite(interval) && interval > 0) {
35
+ return String(Math.round(interval))
36
+ }
37
+ if (interval != null && typeof interval === 'string' && interval.trim()) {
38
+ const t = interval.trim().toLowerCase()
39
+ const n = Number(t)
40
+ if (Number.isFinite(n) && n > 0) return String(Math.round(n))
41
+ const m = t.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/)
42
+ if (m && (m[1] || m[2] || m[3])) {
43
+ const total = (m[1] ? parseInt(m[1]) * 3600 : 0) + (m[2] ? parseInt(m[2]) * 60 : 0) + (m[3] ? parseInt(m[3]) : 0)
44
+ if (total > 0) return String(total)
45
+ }
46
+ }
47
+ return '' // default
48
+ }
14
49
 
15
50
  export function AgentSheet() {
16
51
  const open = useAppStore((s) => s.agentSheetOpen)
@@ -19,6 +54,8 @@ export function AgentSheet() {
19
54
  const setEditingId = useAppStore((s) => s.setEditingAgentId)
20
55
  const agents = useAppStore((s) => s.agents)
21
56
  const loadAgents = useAppStore((s) => s.loadAgents)
57
+ const projects = useAppStore((s) => s.projects)
58
+ const loadProjects = useAppStore((s) => s.loadProjects)
22
59
  const providers = useAppStore((s) => s.providers)
23
60
  const loadProviders = useAppStore((s) => s.loadProviders)
24
61
  const credentials = useAppStore((s) => s.credentials)
@@ -60,9 +97,13 @@ export function AgentSheet() {
60
97
  const [capInput, setCapInput] = useState('')
61
98
  const [ollamaMode, setOllamaMode] = useState<'local' | 'cloud'>('local')
62
99
  const [openclawEnabled, setOpenclawEnabled] = useState(false)
100
+ const [projectId, setProjectId] = useState<string | undefined>(undefined)
101
+ const [avatarSeed, setAvatarSeed] = useState('')
102
+ const [thinkingLevel, setThinkingLevel] = useState<'' | 'minimal' | 'low' | 'medium' | 'high'>('')
63
103
  const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
64
- const [heartbeatInterval, setHeartbeatInterval] = useState('')
104
+ const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
65
105
  const [heartbeatModel, setHeartbeatModel] = useState('')
106
+ const [heartbeatPrompt, setHeartbeatPrompt] = useState('')
66
107
  const [addingKey, setAddingKey] = useState(false)
67
108
  const [newKeyName, setNewKeyName] = useState('')
68
109
  const [newKeyValue, setNewKeyValue] = useState('')
@@ -105,6 +146,7 @@ export function AgentSheet() {
105
146
  loadProviders()
106
147
  loadCredentials()
107
148
  loadSkills()
149
+ loadProjects()
108
150
  loadClaudeSkills()
109
151
  setTestStatus('idle')
110
152
  setTestMessage('')
@@ -130,9 +172,13 @@ export function AgentSheet() {
130
172
  setCapInput('')
131
173
  setOllamaMode(editing.credentialId && editing.provider === 'ollama' ? 'cloud' : 'local')
132
174
  setOpenclawEnabled(editing.provider === 'openclaw')
175
+ setProjectId(editing.projectId)
176
+ setAvatarSeed(editing.avatarSeed || '')
177
+ setThinkingLevel(editing.thinkingLevel || '')
133
178
  setHeartbeatEnabled(editing.heartbeatEnabled || false)
134
- setHeartbeatInterval(editing.heartbeatInterval != null ? String(editing.heartbeatInterval) : '')
179
+ setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
135
180
  setHeartbeatModel(editing.heartbeatModel || '')
181
+ setHeartbeatPrompt(editing.heartbeatPrompt || '')
136
182
  } else {
137
183
  setName('')
138
184
  setDescription('')
@@ -154,9 +200,13 @@ export function AgentSheet() {
154
200
  setCapInput('')
155
201
  setOllamaMode('local')
156
202
  setOpenclawEnabled(false)
203
+ setProjectId(undefined)
204
+ setAvatarSeed('')
205
+ setThinkingLevel('')
157
206
  setHeartbeatEnabled(false)
158
- setHeartbeatInterval('')
207
+ setHeartbeatIntervalSec('')
159
208
  setHeartbeatModel('')
209
+ setHeartbeatPrompt('')
160
210
  }
161
211
  }
162
212
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -244,9 +294,14 @@ export function AgentSheet() {
244
294
  fallbackCredentialIds,
245
295
  platformAssignScope,
246
296
  capabilities,
297
+ projectId: projectId || undefined,
298
+ avatarSeed: avatarSeed.trim() || undefined,
299
+ thinkingLevel: thinkingLevel || undefined,
247
300
  heartbeatEnabled,
248
- heartbeatInterval: heartbeatInterval.trim() || null,
301
+ heartbeatInterval: heartbeatIntervalSec ? formatHbDuration(Number(heartbeatIntervalSec)) : null,
302
+ heartbeatIntervalSec: heartbeatIntervalSec ? Number(heartbeatIntervalSec) : null,
249
303
  heartbeatModel: heartbeatModel.trim() || null,
304
+ heartbeatPrompt: heartbeatPrompt.trim() || null,
250
305
  }
251
306
  if (editing) {
252
307
  await updateAgent(editing.id, data)
@@ -330,8 +385,7 @@ export function AgentSheet() {
330
385
 
331
386
  // Whether this provider needs a connection test before saving.
332
387
  // Only CLI providers (no remote connection) skip the test.
333
- const CLI_ONLY_PROVIDERS: Set<ProviderType> = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
334
- const needsTest = !providerNeedsKey && !CLI_ONLY_PROVIDERS.has(provider)
388
+ const needsTest = !providerNeedsKey && !NON_LANGGRAPH_PROVIDER_IDS.has(provider)
335
389
 
336
390
  const [saving, setSaving] = useState(false)
337
391
 
@@ -402,6 +456,29 @@ export function AgentSheet() {
402
456
  <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. SEO Researcher" className={inputClass} style={{ fontFamily: 'inherit' }} />
403
457
  </div>
404
458
 
459
+ <div className="mb-8">
460
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Avatar</label>
461
+ <div className="flex items-center gap-3">
462
+ <AgentAvatar seed={avatarSeed || null} name={name || 'A'} size={40} />
463
+ <input
464
+ type="text"
465
+ value={avatarSeed}
466
+ onChange={(e) => setAvatarSeed(e.target.value)}
467
+ placeholder="Avatar seed (any text)"
468
+ className={inputClass}
469
+ style={{ fontFamily: 'inherit', flex: 1 }}
470
+ />
471
+ <button
472
+ type="button"
473
+ onClick={() => setAvatarSeed(Math.random().toString(36).slice(2, 10))}
474
+ className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600 cursor-pointer transition-all hover:bg-white/[0.04] shrink-0"
475
+ style={{ fontFamily: 'inherit' }}
476
+ >
477
+ Randomize
478
+ </button>
479
+ </div>
480
+ </div>
481
+
405
482
  <div className="mb-8">
406
483
  <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Description</label>
407
484
  <input type="text" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="What does this agent do?" className={inputClass} style={{ fontFamily: 'inherit' }} />
@@ -451,6 +528,46 @@ export function AgentSheet() {
451
528
  <p className="text-[11px] text-text-3/70 mt-1.5">Press Enter or comma to add. Other agents see these when deciding delegation.</p>
452
529
  </div>}
453
530
 
531
+ {/* Project */}
532
+ {Object.keys(projects).length > 0 && (
533
+ <div className="mb-8">
534
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
535
+ Project <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
536
+ </label>
537
+ <select
538
+ value={projectId || ''}
539
+ onChange={(e) => setProjectId(e.target.value || undefined)}
540
+ className={inputClass}
541
+ style={{ fontFamily: 'inherit' }}
542
+ >
543
+ <option value="">None</option>
544
+ {Object.values(projects).map((p) => (
545
+ <option key={p.id} value={p.id}>{p.name}</option>
546
+ ))}
547
+ </select>
548
+ </div>
549
+ )}
550
+
551
+ {/* Thinking Level */}
552
+ <div className="mb-8">
553
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
554
+ Thinking Level <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
555
+ </label>
556
+ <select
557
+ value={thinkingLevel}
558
+ onChange={(e) => setThinkingLevel(e.target.value as typeof thinkingLevel)}
559
+ className={inputClass}
560
+ style={{ fontFamily: 'inherit' }}
561
+ >
562
+ <option value="">None (default)</option>
563
+ <option value="minimal">Minimal — Direct and concise</option>
564
+ <option value="low">Low — Brief reasoning</option>
565
+ <option value="medium">Medium — Moderate analysis</option>
566
+ <option value="high">High — Deep, thorough reasoning</option>
567
+ </select>
568
+ <p className="text-[11px] text-text-3/70 mt-1.5">Controls reasoning depth. Anthropic models use extended thinking; OpenAI o-series uses reasoning_effort. Others get system prompt guidance.</p>
569
+ </div>
570
+
454
571
  {/* Heartbeat Configuration */}
455
572
  <div className="mb-8">
456
573
  <div className="flex items-center justify-between mb-3">
@@ -467,14 +584,16 @@ export function AgentSheet() {
467
584
  <div className="space-y-4 mt-3">
468
585
  <div>
469
586
  <label className="block text-[12px] text-text-3/70 mb-1.5">Interval</label>
470
- <input
471
- type="text"
472
- value={heartbeatInterval}
473
- onChange={(e) => setHeartbeatInterval(e.target.value)}
474
- placeholder="30s, 5m, 1h (default: 30m)"
587
+ <select
588
+ value={heartbeatIntervalSec}
589
+ onChange={(e) => setHeartbeatIntervalSec(e.target.value)}
475
590
  className={inputClass}
476
- style={{ fontFamily: 'inherit' }}
477
- />
591
+ >
592
+ <option value="">Default (30m)</option>
593
+ {HB_PRESETS.map((sec) => (
594
+ <option key={sec} value={String(sec)}>{formatHbDuration(sec)}</option>
595
+ ))}
596
+ </select>
478
597
  </div>
479
598
  <div>
480
599
  <label className="block text-[12px] text-text-3/70 mb-1.5">Model override <span className="text-text-3/50">(optional, cheaper model)</span></label>
@@ -487,6 +606,17 @@ export function AgentSheet() {
487
606
  style={{ fontFamily: 'inherit' }}
488
607
  />
489
608
  </div>
609
+ <div>
610
+ <label className="block text-[12px] text-text-3/70 mb-1.5">Instructions <span className="text-text-3/50">(what to do each tick)</span></label>
611
+ <textarea
612
+ value={heartbeatPrompt}
613
+ onChange={(e) => setHeartbeatPrompt(e.target.value)}
614
+ placeholder="Describe what this agent should do during heartbeat ticks..."
615
+ rows={4}
616
+ className={`${inputClass} resize-y min-h-[100px]`}
617
+ style={{ fontFamily: 'inherit' }}
618
+ />
619
+ </div>
490
620
  </div>
491
621
  )}
492
622
  <p className="text-[11px] text-text-3/70 mt-1.5">Periodic check-in runs on idle sessions using this agent. Processes pending events and monitors status.</p>
@@ -765,6 +895,8 @@ export function AgentSheet() {
765
895
  </div>
766
896
  )}
767
897
 
898
+ {/* OpenClaw manages its own models — no selector needed */}
899
+
768
900
  {/* Ollama Mode Toggle */}
769
901
  {!openclawEnabled && provider === 'ollama' && (
770
902
  <div className="mb-8">
@@ -1249,3 +1381,4 @@ export function AgentSheet() {
1249
1381
  </BottomSheet>
1250
1382
  )
1251
1383
  }
1384
+