@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
@@ -1,13 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import fs from 'fs'
3
3
  import path from 'path'
4
- import crypto from 'crypto'
4
+ import { genId } from '@/lib/id'
5
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
6
6
 
7
7
  export async function POST(req: Request) {
8
8
  const filename = req.headers.get('x-filename') || 'image.png'
9
9
  const buf = Buffer.from(await req.arrayBuffer())
10
- const name = crypto.randomBytes(4).toString('hex') + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
10
+ const name = genId() + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
11
11
  const filePath = path.join(UPLOAD_DIR, name)
12
12
 
13
13
  if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { notFound } from '@/lib/server/collection-helpers'
2
3
  import fs from 'fs'
3
4
  import path from 'path'
4
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
@@ -43,16 +44,18 @@ export async function GET(_req: Request, { params }: { params: Promise<{ filenam
43
44
  const filePath = path.join(UPLOAD_DIR, safeName)
44
45
 
45
46
  if (!fs.existsSync(filePath)) {
46
- return new NextResponse(null, { status: 404 })
47
+ return notFound()
47
48
  }
48
49
 
49
50
  const ext = path.extname(safeName).toLowerCase()
50
51
  const contentType = MIME_TYPES[ext] || 'application/octet-stream'
51
52
  const data = fs.readFileSync(filePath)
52
53
 
54
+ const inline = contentType.startsWith('image/') || contentType.startsWith('video/') || contentType.startsWith('text/') || contentType === 'application/pdf'
53
55
  return new NextResponse(data, {
54
56
  headers: {
55
57
  'Content-Type': contentType,
58
+ 'Content-Disposition': inline ? 'inline' : `attachment; filename="${path.basename(safeName)}"`,
56
59
  'Cache-Control': 'public, max-age=86400',
57
60
  },
58
61
  })
@@ -1,8 +1,12 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadAgents, loadSessions, loadWebhooks, saveSessions, saveWebhooks, appendWebhookLog } from '@/lib/server/storage'
4
4
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
5
  import { enqueueSessionRun } from '@/lib/server/session-run-manager'
6
+ import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
7
+
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ const ops: CollectionOps<any> = { load: loadWebhooks, save: saveWebhooks }
6
10
 
7
11
  function normalizeEvents(value: unknown): string[] {
8
12
  if (!Array.isArray(value)) return []
@@ -22,36 +26,30 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
22
26
  const { id } = await params
23
27
  const webhooks = loadWebhooks()
24
28
  const webhook = webhooks[id]
25
- if (!webhook) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
29
+ if (!webhook) return notFound('Webhook not found')
26
30
  return NextResponse.json(webhook)
27
31
  }
28
32
 
29
33
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
30
34
  const { id } = await params
31
35
  const body = await req.json().catch(() => ({}))
32
- const webhooks = loadWebhooks()
33
- const webhook = webhooks[id]
34
- if (!webhook) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
35
-
36
- if (body.name !== undefined) webhook.name = body.name
37
- if (body.source !== undefined) webhook.source = body.source
38
- if (body.events !== undefined) webhook.events = normalizeEvents(body.events)
39
- if (body.agentId !== undefined) webhook.agentId = body.agentId
40
- if (body.secret !== undefined) webhook.secret = body.secret
41
- if (body.isEnabled !== undefined) webhook.isEnabled = !!body.isEnabled
42
- webhook.updatedAt = Date.now()
43
-
44
- webhooks[id] = webhook
45
- saveWebhooks(webhooks)
46
- return NextResponse.json(webhook)
36
+ const result = mutateItem(ops, id, (webhook) => {
37
+ if (body.name !== undefined) webhook.name = body.name
38
+ if (body.source !== undefined) webhook.source = body.source
39
+ if (body.events !== undefined) webhook.events = normalizeEvents(body.events)
40
+ if (body.agentId !== undefined) webhook.agentId = body.agentId
41
+ if (body.secret !== undefined) webhook.secret = body.secret
42
+ if (body.isEnabled !== undefined) webhook.isEnabled = !!body.isEnabled
43
+ webhook.updatedAt = Date.now()
44
+ return webhook
45
+ })
46
+ if (!result) return notFound('Webhook not found')
47
+ return NextResponse.json(result)
47
48
  }
48
49
 
49
50
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
50
51
  const { id } = await params
51
- const webhooks = loadWebhooks()
52
- if (!webhooks[id]) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
53
- delete webhooks[id]
54
- saveWebhooks(webhooks)
52
+ if (!deleteItem(ops, id)) return notFound('Webhook not found')
55
53
  return NextResponse.json({ ok: true })
56
54
  }
57
55
 
@@ -59,10 +57,10 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
59
57
  const { id } = await params
60
58
  const webhooks = loadWebhooks()
61
59
  const webhook = webhooks[id]
62
- if (!webhook) return NextResponse.json({ error: 'Webhook not found' }, { status: 404 })
60
+ if (!webhook) return notFound('Webhook not found')
63
61
  if (webhook.isEnabled === false) {
64
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
65
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: 'unknown',
62
+ appendWebhookLog(genId(8), {
63
+ id: genId(8), webhookId: id, event: 'unknown',
66
64
  payload: '', status: 'error', error: 'Webhook is disabled', timestamp: Date.now(),
67
65
  })
68
66
  return NextResponse.json({ error: 'Webhook is disabled' }, { status: 409 })
@@ -73,8 +71,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
73
71
  const url = new URL(req.url)
74
72
  const provided = req.headers.get('x-webhook-secret') || url.searchParams.get('secret') || ''
75
73
  if (provided !== secret) {
76
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
77
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: 'unknown',
74
+ appendWebhookLog(genId(8), {
75
+ id: genId(8), webhookId: id, event: 'unknown',
78
76
  payload: '', status: 'error', error: 'Invalid webhook secret', timestamp: Date.now(),
79
77
  })
80
78
  return NextResponse.json({ error: 'Invalid webhook secret' }, { status: 401 })
@@ -122,8 +120,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
122
120
  const agents = loadAgents()
123
121
  const agent = webhook.agentId ? agents[webhook.agentId] : null
124
122
  if (!agent) {
125
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
126
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: incomingEvent,
123
+ appendWebhookLog(genId(8), {
124
+ id: genId(8), webhookId: id, event: incomingEvent,
127
125
  payload: (rawBody || '').slice(0, 2000), status: 'error', error: 'Webhook agent is not configured or missing', timestamp: Date.now(),
128
126
  })
129
127
  return NextResponse.json({ error: 'Webhook agent is not configured or missing' }, { status: 400 })
@@ -133,7 +131,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
133
131
  const sessionName = `webhook:${id}`
134
132
  let session = Object.values(sessions).find((s: any) => s.name === sessionName && s.agentId === agent.id) as any
135
133
  if (!session) {
136
- const sessionId = crypto.randomBytes(4).toString('hex')
134
+ const sessionId = genId()
137
135
  const now = Date.now()
138
136
  session = {
139
137
  id: sessionId,
@@ -189,8 +187,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
189
187
  mode: 'followup',
190
188
  })
191
189
 
192
- appendWebhookLog(crypto.randomBytes(8).toString('hex'), {
193
- id: crypto.randomBytes(8).toString('hex'), webhookId: id, event: incomingEvent,
190
+ appendWebhookLog(genId(8), {
191
+ id: genId(8), webhookId: id, event: incomingEvent,
194
192
  payload: (rawBody || '').slice(0, 2000), status: 'success',
195
193
  sessionId: session.id, runId: run.runId, timestamp: Date.now(),
196
194
  })
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadWebhooks, saveWebhooks } from '@/lib/server/storage'
4
4
  export const dynamic = 'force-dynamic'
@@ -19,7 +19,7 @@ export async function GET(_req: Request) {
19
19
  export async function POST(req: Request) {
20
20
  const body = await req.json().catch(() => ({}))
21
21
  const webhooks = loadWebhooks()
22
- const id = crypto.randomBytes(4).toString('hex')
22
+ const id = genId()
23
23
  const now = Date.now()
24
24
 
25
25
  webhooks[id] = {
@@ -79,6 +79,9 @@
79
79
  }
80
80
 
81
81
  :root {
82
+ --font-dm-sans: 'Segoe UI';
83
+ --font-sora: 'Segoe UI';
84
+ --font-jetbrains-mono: 'SF Mono';
82
85
  --radius: 0.625rem;
83
86
  --background: #08080d;
84
87
  --foreground: #e2e2ec;
@@ -203,6 +206,17 @@ body {
203
206
  100% { background-position: 0% 50%; }
204
207
  }
205
208
 
209
+ /* AI avatar mood animations */
210
+ @keyframes ai-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.15); } }
211
+ @keyframes ai-glow { 0%,100% { box-shadow: 0 0 0 0 rgba(99,102,241,0); } 50% { box-shadow: 0 0 12px 4px rgba(99,102,241,0.35); } }
212
+ @keyframes ai-shake { 0%,100% { transform: translateX(0); } 20% { transform: translateX(-3px); } 40% { transform: translateX(3px); } 60% { transform: translateX(-2px); } 80% { transform: translateX(2px); } }
213
+ @keyframes ai-bounce { 0%,100% { transform: scale(1); } 40% { transform: scale(1.25); } 60% { transform: scale(0.95); } }
214
+
215
+ .ai-mood-pulse { animation: ai-pulse 2s ease-in-out infinite; }
216
+ .ai-mood-glow { animation: ai-glow 1.8s ease-in-out infinite; }
217
+ .ai-mood-shake { animation: ai-shake 0.5s ease-in-out; }
218
+ .ai-mood-bounce { animation: ai-bounce 0.6s ease-out; }
219
+
206
220
  /* ===== Markdown content ===== */
207
221
  .msg-content pre {
208
222
  background: #0a0a12 !important;
@@ -1,27 +1,8 @@
1
1
  import type { Metadata, Viewport } from "next"
2
- import { Sora, DM_Sans, JetBrains_Mono } from "next/font/google"
3
2
  import { TooltipProvider } from "@/components/ui/tooltip"
4
3
  import { Toaster } from "@/components/ui/sonner"
5
4
  import "./globals.css"
6
5
 
7
- const sora = Sora({
8
- variable: "--font-sora",
9
- subsets: ["latin"],
10
- weight: ["400", "500", "600", "700", "800"],
11
- })
12
-
13
- const dmSans = DM_Sans({
14
- variable: "--font-dm-sans",
15
- subsets: ["latin"],
16
- weight: ["400", "500", "600", "700"],
17
- })
18
-
19
- const jetbrainsMono = JetBrains_Mono({
20
- variable: "--font-jetbrains-mono",
21
- subsets: ["latin"],
22
- weight: ["400", "500"],
23
- })
24
-
25
6
  export const metadata: Metadata = {
26
7
  title: "SwarmClaw",
27
8
  description: "AI agent orchestration dashboard with multi-provider support",
@@ -34,6 +15,10 @@ export const viewport: Viewport = {
34
15
  viewportFit: "cover",
35
16
  }
36
17
 
18
+ // Avoid static prerendering for the app shell. This prevents flaky
19
+ // Turbopack prerender failures seen in detached fresh-install builds.
20
+ export const dynamic = "force-dynamic"
21
+
37
22
  export default function RootLayout({
38
23
  children,
39
24
  }: Readonly<{
@@ -41,7 +26,7 @@ export default function RootLayout({
41
26
  }>) {
42
27
  return (
43
28
  <html lang="en" className="dark">
44
- <body className={`${sora.variable} ${dmSans.variable} ${jetbrainsMono.variable} antialiased`} cz-shortcut-listen="true">
29
+ <body className="antialiased" cz-shortcut-listen="true">
45
30
  <TooltipProvider>
46
31
  {children}
47
32
  <Toaster />
package/src/app/page.tsx CHANGED
@@ -10,6 +10,7 @@ import { AccessKeyGate } from '@/components/auth/access-key-gate'
10
10
  import { UserPicker } from '@/components/auth/user-picker'
11
11
  import { SetupWizard } from '@/components/auth/setup-wizard'
12
12
  import { AppLayout } from '@/components/layout/app-layout'
13
+ import { useViewRouter } from '@/hooks/use-view-router'
13
14
 
14
15
  export default function Home() {
15
16
  const currentUser = useAppStore((s) => s.currentUser)
@@ -17,7 +18,6 @@ export default function Home() {
17
18
  const hydrated = useAppStore((s) => s._hydrated)
18
19
  const hydrate = useAppStore((s) => s.hydrate)
19
20
  const loadNetworkInfo = useAppStore((s) => s.loadNetworkInfo)
20
- const sessions = useAppStore((s) => s.sessions)
21
21
  const loadSessions = useAppStore((s) => s.loadSessions)
22
22
  const loadSettings = useAppStore((s) => s.loadSettings)
23
23
 
@@ -107,29 +107,6 @@ export default function Home() {
107
107
  return () => { cancelled = true }
108
108
  }, [authenticated, currentUser])
109
109
 
110
- // Keep __main__ session for backward compat — create if missing
111
- useEffect(() => {
112
- if (!authenticated || !currentUser) return
113
- const sessionList = Object.values(sessions)
114
- const mainSession = sessionList.find((s: any) => s.name === '__main__' && s.user === currentUser)
115
- if (mainSession) return
116
- let cancelled = false
117
- ;(async () => {
118
- try {
119
- const mainId = `main-${currentUser}`
120
- await api<any>('POST', '/sessions', {
121
- id: mainId,
122
- name: '__main__',
123
- user: currentUser,
124
- agentId: 'default',
125
- heartbeatEnabled: true,
126
- })
127
- if (!cancelled) await loadSessions()
128
- } catch { /* ignore */ }
129
- })()
130
- return () => { cancelled = true }
131
- }, [authenticated, currentUser, sessions, loadSessions])
132
-
133
110
  // Check if first-run setup is needed
134
111
  useEffect(() => {
135
112
  if (!authenticated || !currentUser) return
@@ -169,6 +146,8 @@ export default function Home() {
169
146
  return () => window.removeEventListener('sc_auth_required', handler)
170
147
  }, [])
171
148
 
149
+ useViewRouter()
150
+
172
151
  if (!hydrated || !authChecked) return null
173
152
  if (!authenticated) return <AccessKeyGate onAuthenticated={() => setAuthenticated(true)} />
174
153
  if (!currentUser) return <UserPicker />
package/src/cli/index.js CHANGED
@@ -17,6 +17,9 @@ const COMMAND_GROUPS = [
17
17
  cmd('create', 'POST', '/agents', 'Create an agent', { expectsJsonBody: true }),
18
18
  cmd('update', 'PUT', '/agents/:id', 'Update an agent', { expectsJsonBody: true }),
19
19
  cmd('delete', 'DELETE', '/agents/:id', 'Delete an agent'),
20
+ cmd('trash', 'GET', '/agents/trash', 'List trashed agents'),
21
+ cmd('restore', 'POST', '/agents/trash', 'Restore a trashed agent', { expectsJsonBody: true }),
22
+ cmd('purge', 'DELETE', '/agents/trash', 'Permanently delete a trashed agent', { expectsJsonBody: true }),
20
23
  cmd('thread', 'POST', '/agents/:id/thread', 'Get or create agent thread session'),
21
24
  ],
22
25
  },
@@ -55,6 +58,7 @@ const COMMAND_GROUPS = [
55
58
  cmd('create', 'POST', '/connectors', 'Create connector', { expectsJsonBody: true }),
56
59
  cmd('update', 'PUT', '/connectors/:id', 'Update connector', { expectsJsonBody: true }),
57
60
  cmd('delete', 'DELETE', '/connectors/:id', 'Delete connector'),
61
+ cmd('webhook', 'POST', '/connectors/:id/webhook', 'Trigger connector webhook ingress', { expectsJsonBody: true }),
58
62
  cmd('start', 'PUT', '/connectors/:id', 'Start connector', {
59
63
  expectsJsonBody: true,
60
64
  defaultBody: { action: 'start' },
@@ -199,6 +203,42 @@ const COMMAND_GROUPS = [
199
203
  expectsJsonBody: true,
200
204
  waitEntityFrom: 'taskId',
201
205
  }),
206
+ cmd('graph', 'GET', '/orchestrator/graph', 'Get orchestrator graph structure'),
207
+ ],
208
+ },
209
+ {
210
+ name: 'openclaw',
211
+ description: 'OpenClaw discovery, gateway control, and runtime APIs',
212
+ commands: [
213
+ cmd('discover', 'GET', '/openclaw/discover', 'Discover OpenClaw gateways'),
214
+ cmd('directory', 'GET', '/openclaw/directory', 'List directory entries from running OpenClaw connectors'),
215
+ cmd('gateway-status', 'GET', '/openclaw/gateway', 'Check OpenClaw gateway connection status'),
216
+ cmd('gateway', 'POST', '/openclaw/gateway', 'Call OpenClaw gateway RPC/control action', { expectsJsonBody: true }),
217
+ cmd('config-sync', 'GET', '/openclaw/config-sync', 'Detect OpenClaw gateway config issues'),
218
+ cmd('config-sync-repair', 'POST', '/openclaw/config-sync', 'Repair a detected OpenClaw config issue', { expectsJsonBody: true }),
219
+ cmd('approvals', 'GET', '/openclaw/approvals', 'List pending OpenClaw execution approvals'),
220
+ cmd('approvals-resolve', 'POST', '/openclaw/approvals', 'Resolve an OpenClaw execution approval', { expectsJsonBody: true }),
221
+ cmd('cron', 'GET', '/openclaw/cron', 'List OpenClaw cron jobs'),
222
+ cmd('cron-action', 'POST', '/openclaw/cron', 'Create/run/remove OpenClaw cron jobs', { expectsJsonBody: true }),
223
+ cmd('agent-files', 'GET', '/openclaw/agent-files', 'Fetch OpenClaw agent files'),
224
+ cmd('agent-files-set', 'PUT', '/openclaw/agent-files', 'Save an OpenClaw agent file', { expectsJsonBody: true }),
225
+ cmd('dotenv-keys', 'GET', '/openclaw/dotenv-keys', 'List gateway .env keys'),
226
+ cmd('exec-config', 'GET', '/openclaw/exec-config', 'Fetch OpenClaw exec approval config'),
227
+ cmd('exec-config-set', 'PUT', '/openclaw/exec-config', 'Save OpenClaw exec approval config', { expectsJsonBody: true }),
228
+ cmd('history-preview', 'GET', '/openclaw/history', 'Preview OpenClaw session history'),
229
+ cmd('history-merge', 'POST', '/openclaw/history', 'Merge OpenClaw session history into local session', { expectsJsonBody: true }),
230
+ cmd('media', 'GET', '/openclaw/media', 'Proxy OpenClaw media/file content'),
231
+ cmd('models', 'GET', '/openclaw/models', 'List allowed OpenClaw models'),
232
+ cmd('permissions', 'GET', '/openclaw/permissions', 'Get OpenClaw permission preset/config'),
233
+ cmd('permissions-set', 'PUT', '/openclaw/permissions', 'Apply OpenClaw permission preset', { expectsJsonBody: true }),
234
+ cmd('sandbox-env', 'GET', '/openclaw/sandbox-env', 'List OpenClaw sandbox env allowlist'),
235
+ cmd('sandbox-env-set', 'PUT', '/openclaw/sandbox-env', 'Update OpenClaw sandbox env allowlist', { expectsJsonBody: true }),
236
+ cmd('skills', 'GET', '/openclaw/skills', 'List OpenClaw skills and eligibility'),
237
+ cmd('skills-update', 'PATCH', '/openclaw/skills', 'Update OpenClaw skill state/config', { expectsJsonBody: true }),
238
+ cmd('skills-save', 'PUT', '/openclaw/skills', 'Save OpenClaw skill allowlist mode/config', { expectsJsonBody: true }),
239
+ cmd('skills-install', 'POST', '/openclaw/skills/install', 'Install OpenClaw skill dependencies', { expectsJsonBody: true }),
240
+ cmd('skills-remove', 'POST', '/openclaw/skills/remove', 'Remove OpenClaw skill', { expectsJsonBody: true }),
241
+ cmd('sync', 'POST', '/openclaw/sync', 'Run OpenClaw sync action', { expectsJsonBody: true }),
202
242
  ],
203
243
  },
204
244
  {
@@ -208,6 +248,17 @@ const COMMAND_GROUPS = [
208
248
  cmd('manage', 'POST', '/preview-server', 'Start/stop/status/detect preview server', { expectsJsonBody: true }),
209
249
  ],
210
250
  },
251
+ {
252
+ name: 'projects',
253
+ description: 'Manage projects',
254
+ commands: [
255
+ cmd('list', 'GET', '/projects', 'List projects'),
256
+ cmd('get', 'GET', '/projects/:id', 'Get project by id'),
257
+ cmd('create', 'POST', '/projects', 'Create project', { expectsJsonBody: true }),
258
+ cmd('update', 'PUT', '/projects/:id', 'Update project', { expectsJsonBody: true }),
259
+ cmd('delete', 'DELETE', '/projects/:id', 'Delete project'),
260
+ ],
261
+ },
211
262
  {
212
263
  name: 'plugins',
213
264
  description: 'Manage plugins and marketplace',
@@ -281,6 +332,9 @@ const COMMAND_GROUPS = [
281
332
  defaultBody: { action: 'disable_all' },
282
333
  }),
283
334
  cmd('messages', 'GET', '/sessions/:id/messages', 'Get session messages'),
335
+ cmd('messages-update', 'PUT', '/sessions/:id/messages', 'Update session message metadata (e.g. bookmark)', { expectsJsonBody: true }),
336
+ cmd('fork', 'POST', '/sessions/:id/fork', 'Fork session from a specific message index', { expectsJsonBody: true }),
337
+ cmd('edit-resend', 'POST', '/sessions/:id/edit-resend', 'Edit and resend from a specific message index', { expectsJsonBody: true }),
284
338
  cmd('main-loop', 'GET', '/sessions/:id/main-loop', 'Get main mission loop state'),
285
339
  cmd('main-loop-action', 'POST', '/sessions/:id/main-loop', 'Control main mission loop (pause/resume/set_goal/set_mode/clear_events/nudge)', {
286
340
  expectsJsonBody: true,
@@ -351,6 +405,7 @@ const COMMAND_GROUPS = [
351
405
  cmd('update', 'PUT', '/tasks/:id', 'Update task', { expectsJsonBody: true }),
352
406
  cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'),
353
407
  cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }),
408
+ cmd('approve', 'POST', '/tasks/:id/approve', 'Approve or reject a pending tool execution', { expectsJsonBody: true }),
354
409
  ],
355
410
  },
356
411
  {
@@ -362,6 +417,11 @@ const COMMAND_GROUPS = [
362
417
  responseType: 'binary',
363
418
  bodyFlagMap: { text: 'text' },
364
419
  }),
420
+ cmd('stream', 'POST', '/tts/stream', 'Generate streaming TTS audio', {
421
+ expectsJsonBody: true,
422
+ responseType: 'binary',
423
+ bodyFlagMap: { text: 'text' },
424
+ }),
365
425
  ],
366
426
  },
367
427
  {
package/src/cli/index.ts CHANGED
@@ -928,7 +928,7 @@ export function buildProgram(): Command {
928
928
  connectors
929
929
  .command('create')
930
930
  .description('Create connector')
931
- .requiredOption('--platform <platform>', 'Connector platform (discord|telegram|slack|whatsapp|openclaw)')
931
+ .requiredOption('--platform <platform>', 'Connector platform (discord|telegram|slack|whatsapp|openclaw|bluebubbles|signal|teams|googlechat|matrix)')
932
932
  .requiredOption('--agent-id <agentId>', 'Agent id')
933
933
  .option('--name <name>', 'Connector name')
934
934
  .option('--credential-id <credentialId>', 'Credential id')
package/src/cli/spec.js CHANGED
@@ -7,6 +7,9 @@ const COMMAND_GROUPS = {
7
7
  create: { description: 'Create an agent', method: 'POST', path: '/agents' },
8
8
  update: { description: 'Update an agent', method: 'PUT', path: '/agents/:id', params: ['id'] },
9
9
  delete: { description: 'Delete an agent', method: 'DELETE', path: '/agents/:id', params: ['id'] },
10
+ trash: { description: 'List trashed agents', method: 'GET', path: '/agents/trash' },
11
+ restore: { description: 'Restore a trashed agent', method: 'POST', path: '/agents/trash' },
12
+ purge: { description: 'Permanently delete a trashed agent', method: 'DELETE', path: '/agents/trash' },
10
13
  },
11
14
  },
12
15
  auth: {
@@ -127,6 +130,41 @@ const COMMAND_GROUPS = {
127
130
  run: { description: 'Run orchestrator task now', method: 'POST', path: '/orchestrator/run', waitable: true },
128
131
  runs: { description: 'List queued/running/completed runs', method: 'GET', path: '/runs' },
129
132
  'run-get': { description: 'Get run by id', method: 'GET', path: '/runs/:id', params: ['id'] },
133
+ graph: { description: 'Get orchestrator graph structure', method: 'GET', path: '/orchestrator/graph' },
134
+ },
135
+ },
136
+ openclaw: {
137
+ description: 'OpenClaw discovery, gateway control, and runtime APIs',
138
+ commands: {
139
+ discover: { description: 'Discover OpenClaw gateways', method: 'GET', path: '/openclaw/discover' },
140
+ directory: { description: 'List directory entries from running OpenClaw connectors', method: 'GET', path: '/openclaw/directory' },
141
+ 'gateway-status': { description: 'Check OpenClaw gateway connection status', method: 'GET', path: '/openclaw/gateway' },
142
+ gateway: { description: 'Call OpenClaw gateway RPC/control action', method: 'POST', path: '/openclaw/gateway' },
143
+ 'config-sync': { description: 'Detect OpenClaw gateway config issues', method: 'GET', path: '/openclaw/config-sync' },
144
+ 'config-sync-repair': { description: 'Repair a detected OpenClaw config issue', method: 'POST', path: '/openclaw/config-sync' },
145
+ approvals: { description: 'List pending OpenClaw execution approvals', method: 'GET', path: '/openclaw/approvals' },
146
+ 'approvals-resolve': { description: 'Resolve an OpenClaw execution approval', method: 'POST', path: '/openclaw/approvals' },
147
+ cron: { description: 'List OpenClaw cron jobs', method: 'GET', path: '/openclaw/cron' },
148
+ 'cron-action': { description: 'Create/run/remove OpenClaw cron jobs', method: 'POST', path: '/openclaw/cron' },
149
+ 'agent-files': { description: 'Fetch OpenClaw agent files', method: 'GET', path: '/openclaw/agent-files' },
150
+ 'agent-files-set': { description: 'Save an OpenClaw agent file', method: 'PUT', path: '/openclaw/agent-files' },
151
+ 'dotenv-keys': { description: 'List gateway .env keys', method: 'GET', path: '/openclaw/dotenv-keys' },
152
+ 'exec-config': { description: 'Fetch OpenClaw exec approval config', method: 'GET', path: '/openclaw/exec-config' },
153
+ 'exec-config-set': { description: 'Save OpenClaw exec approval config', method: 'PUT', path: '/openclaw/exec-config' },
154
+ 'history-preview': { description: 'Preview OpenClaw session history', method: 'GET', path: '/openclaw/history' },
155
+ 'history-merge': { description: 'Merge OpenClaw session history into local session', method: 'POST', path: '/openclaw/history' },
156
+ media: { description: 'Proxy OpenClaw media/file content', method: 'GET', path: '/openclaw/media' },
157
+ models: { description: 'List allowed OpenClaw models', method: 'GET', path: '/openclaw/models' },
158
+ permissions: { description: 'Get OpenClaw permission preset/config', method: 'GET', path: '/openclaw/permissions' },
159
+ 'permissions-set': { description: 'Apply OpenClaw permission preset', method: 'PUT', path: '/openclaw/permissions' },
160
+ 'sandbox-env': { description: 'List OpenClaw sandbox env allowlist', method: 'GET', path: '/openclaw/sandbox-env' },
161
+ 'sandbox-env-set': { description: 'Update OpenClaw sandbox env allowlist', method: 'PUT', path: '/openclaw/sandbox-env' },
162
+ skills: { description: 'List OpenClaw skills and eligibility', method: 'GET', path: '/openclaw/skills' },
163
+ 'skills-update': { description: 'Update OpenClaw skill state/config', method: 'PATCH', path: '/openclaw/skills' },
164
+ 'skills-save': { description: 'Save OpenClaw skill allowlist mode/config', method: 'PUT', path: '/openclaw/skills' },
165
+ 'skills-install': { description: 'Install OpenClaw skill dependencies', method: 'POST', path: '/openclaw/skills/install' },
166
+ 'skills-remove': { description: 'Remove OpenClaw skill', method: 'POST', path: '/openclaw/skills/remove' },
167
+ sync: { description: 'Run OpenClaw sync action', method: 'POST', path: '/openclaw/sync' },
130
168
  },
131
169
  },
132
170
  plugins: {
@@ -186,6 +224,9 @@ const COMMAND_GROUPS = {
186
224
  'delete-many': { description: 'Delete multiple sessions (body: {"ids":[...]})', method: 'DELETE', path: '/sessions' },
187
225
  'heartbeat-disable-all': { description: 'Disable all session heartbeats and cancel queued heartbeat runs', method: 'POST', path: '/sessions/heartbeat' },
188
226
  messages: { description: 'Get session message history', method: 'GET', path: '/sessions/:id/messages', params: ['id'] },
227
+ 'messages-update': { description: 'Update session message metadata (e.g. bookmark)', method: 'PUT', path: '/sessions/:id/messages', params: ['id'] },
228
+ fork: { description: 'Fork session from a specific message index', method: 'POST', path: '/sessions/:id/fork', params: ['id'] },
229
+ 'edit-resend': { description: 'Edit and resend from a specific message index', method: 'POST', path: '/sessions/:id/edit-resend', params: ['id'] },
189
230
  'main-loop': { description: 'Get main mission loop state for a session', method: 'GET', path: '/sessions/:id/main-loop', params: ['id'] },
190
231
  'main-loop-action': { description: 'Control main mission loop (pause/resume/set_goal/set_mode/clear_events/nudge)', method: 'POST', path: '/sessions/:id/main-loop', params: ['id'] },
191
232
  chat: { description: 'Send chat message (SSE stream)', method: 'POST', path: '/sessions/:id/chat', params: ['id'], stream: true, waitable: true },
@@ -243,6 +284,7 @@ const COMMAND_GROUPS = {
243
284
  update: { description: 'Update task', method: 'PUT', path: '/tasks/:id', params: ['id'] },
244
285
  delete: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
245
286
  archive: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
287
+ approve: { description: 'Approve or reject a pending tool execution', method: 'POST', path: '/tasks/:id/approve', params: ['id'] },
246
288
  },
247
289
  },
248
290
  webhooks: {
@@ -0,0 +1,45 @@
1
+ 'use client'
2
+
3
+ import { useMemo } from 'react'
4
+ import multiavatar from '@multiavatar/multiavatar'
5
+
6
+ interface Props {
7
+ seed?: string | null
8
+ name: string
9
+ size?: number
10
+ className?: string
11
+ }
12
+
13
+ export function AgentAvatar({ seed, name, size = 32, className = '' }: Props) {
14
+ const svgHtml = useMemo(() => {
15
+ if (!seed) return null
16
+ return multiavatar(seed)
17
+ }, [seed])
18
+
19
+ if (svgHtml) {
20
+ return (
21
+ <div
22
+ className={`shrink-0 rounded-full overflow-hidden ${className}`}
23
+ style={{ width: size, height: size }}
24
+ dangerouslySetInnerHTML={{ __html: svgHtml }}
25
+ />
26
+ )
27
+ }
28
+
29
+ // Fallback: initials
30
+ const initials = name
31
+ .split(/\s+/)
32
+ .slice(0, 2)
33
+ .map((w) => w[0] || '')
34
+ .join('')
35
+ .toUpperCase()
36
+
37
+ return (
38
+ <div
39
+ className={`shrink-0 rounded-full flex items-center justify-center bg-accent-soft text-accent-bright font-600 ${className}`}
40
+ style={{ width: size, height: size, fontSize: size * 0.38 }}
41
+ >
42
+ {initials || '?'}
43
+ </div>
44
+ )
45
+ }
@@ -15,14 +15,17 @@ import {
15
15
  DropdownMenuTrigger,
16
16
  } from '@/components/ui/dropdown-menu'
17
17
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
18
+ import { useApprovalStore } from '@/stores/use-approval-store'
19
+ import { AgentAvatar } from './agent-avatar'
18
20
 
19
21
  interface Props {
20
22
  agent: Agent
21
23
  isDefault?: boolean
24
+ isRunning?: boolean
22
25
  onSetDefault?: (id: string) => void
23
26
  }
24
27
 
25
- export function AgentCard({ agent, isDefault, onSetDefault }: Props) {
28
+ export function AgentCard({ agent, isDefault, isRunning, onSetDefault }: Props) {
26
29
  const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
27
30
  const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
28
31
  const loadSessions = useAppStore((s) => s.loadSessions)
@@ -34,6 +37,8 @@ export function AgentCard({ agent, isDefault, onSetDefault }: Props) {
34
37
  const [dialogOpen, setDialogOpen] = useState(false)
35
38
  const [taskInput, setTaskInput] = useState('')
36
39
  const [confirmDelete, setConfirmDelete] = useState(false)
40
+ const approvals = useApprovalStore((s) => s.approvals)
41
+ const pendingApprovalCount = Object.values(approvals).filter((a) => a.agentId === agent.id).length
37
42
 
38
43
  const handleClick = () => {
39
44
  setEditingAgentId(agent.id)
@@ -111,13 +116,22 @@ export function AgentCard({ agent, isDefault, onSetDefault }: Props) {
111
116
  onClick={() => setConfirmDelete(true)}
112
117
  className="text-red-400 focus:text-red-400"
113
118
  >
114
- Delete
119
+ Move to Trash
115
120
  </DropdownMenuItem>
116
121
  </DropdownMenuContent>
117
122
  </DropdownMenu>
118
123
 
119
124
  <div className="flex items-center gap-2.5">
125
+ <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={28} />
126
+ {isRunning && (
127
+ <span className="shrink-0 w-2 h-2 rounded-full bg-emerald-400" style={{ animation: 'pulse 2s ease infinite' }} title="Running" />
128
+ )}
120
129
  <span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{agent.name}</span>
130
+ {pendingApprovalCount > 0 && (
131
+ <span className="shrink-0 inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-red-500 text-white text-[10px] font-700">
132
+ {pendingApprovalCount}
133
+ </span>
134
+ )}
121
135
  {isDefault && (
122
136
  <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-accent-bright bg-accent-soft px-2 py-0.5 rounded-[6px]">
123
137
  default
@@ -207,9 +221,9 @@ export function AgentCard({ agent, isDefault, onSetDefault }: Props) {
207
221
 
208
222
  <ConfirmDialog
209
223
  open={confirmDelete}
210
- title="Delete Agent"
211
- message={`Are you sure you want to delete "${agent.name}"? This cannot be undone.`}
212
- confirmLabel="Delete"
224
+ title="Move to Trash"
225
+ message={`Move "${agent.name}" to trash? You can restore it later from the trash.`}
226
+ confirmLabel="Move to Trash"
213
227
  danger
214
228
  onConfirm={handleDelete}
215
229
  onCancel={() => setConfirmDelete(false)}