@swarmclawai/swarmclaw 0.4.5 → 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 (113) hide show
  1. package/README.md +8 -2
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +5 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +23 -5
  6. package/src/app/api/agents/trash/route.ts +44 -0
  7. package/src/app/api/connectors/[id]/route.ts +7 -4
  8. package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
  9. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  10. package/src/app/api/openclaw/approvals/route.ts +46 -0
  11. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  12. package/src/app/api/openclaw/cron/route.ts +52 -0
  13. package/src/app/api/openclaw/directory/route.ts +4 -3
  14. package/src/app/api/openclaw/discover/route.ts +3 -2
  15. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  16. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  17. package/src/app/api/openclaw/gateway/route.ts +72 -0
  18. package/src/app/api/openclaw/history/route.ts +109 -0
  19. package/src/app/api/openclaw/media/route.ts +53 -0
  20. package/src/app/api/openclaw/models/route.ts +12 -0
  21. package/src/app/api/openclaw/permissions/route.ts +39 -0
  22. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  23. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  24. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  25. package/src/app/api/openclaw/skills/route.ts +82 -0
  26. package/src/app/api/openclaw/sync/route.ts +3 -2
  27. package/src/app/api/projects/[id]/route.ts +1 -1
  28. package/src/app/api/projects/route.ts +1 -1
  29. package/src/app/api/secrets/[id]/route.ts +1 -1
  30. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  31. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  32. package/src/app/api/sessions/[id]/messages/route.ts +18 -1
  33. package/src/app/api/sessions/[id]/route.ts +12 -3
  34. package/src/app/api/sessions/route.ts +6 -2
  35. package/src/app/globals.css +14 -0
  36. package/src/app/layout.tsx +5 -20
  37. package/src/cli/index.js +33 -1
  38. package/src/cli/spec.js +40 -0
  39. package/src/components/agents/agent-avatar.tsx +45 -0
  40. package/src/components/agents/agent-card.tsx +19 -5
  41. package/src/components/agents/agent-chat-list.tsx +31 -24
  42. package/src/components/agents/agent-files-editor.tsx +185 -0
  43. package/src/components/agents/agent-list.tsx +82 -3
  44. package/src/components/agents/agent-sheet.tsx +31 -0
  45. package/src/components/agents/cron-job-form.tsx +137 -0
  46. package/src/components/agents/exec-config-panel.tsx +147 -0
  47. package/src/components/agents/inspector-panel.tsx +310 -0
  48. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  49. package/src/components/agents/permission-preset-selector.tsx +79 -0
  50. package/src/components/agents/personality-builder.tsx +111 -0
  51. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  52. package/src/components/agents/skill-install-dialog.tsx +102 -0
  53. package/src/components/agents/trash-list.tsx +109 -0
  54. package/src/components/chat/chat-area.tsx +14 -2
  55. package/src/components/chat/chat-header.tsx +168 -4
  56. package/src/components/chat/chat-preview-panel.tsx +113 -0
  57. package/src/components/chat/exec-approval-card.tsx +89 -0
  58. package/src/components/chat/message-bubble.tsx +218 -36
  59. package/src/components/chat/message-list.tsx +135 -31
  60. package/src/components/chat/streaming-bubble.tsx +59 -10
  61. package/src/components/chat/suggestions-bar.tsx +74 -0
  62. package/src/components/chat/thinking-indicator.tsx +20 -6
  63. package/src/components/chat/tool-call-bubble.tsx +89 -16
  64. package/src/components/chat/tool-request-banner.tsx +20 -2
  65. package/src/components/chat/trace-block.tsx +103 -0
  66. package/src/components/projects/project-list.tsx +1 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  68. package/src/components/shared/avatar.tsx +13 -2
  69. package/src/components/shared/settings/settings-page.tsx +1 -0
  70. package/src/components/tasks/task-board.tsx +1 -1
  71. package/src/components/tasks/task-sheet.tsx +12 -12
  72. package/src/hooks/use-continuous-speech.ts +42 -5
  73. package/src/hooks/use-openclaw-gateway.ts +63 -0
  74. package/src/lib/notification-sounds.ts +58 -0
  75. package/src/lib/personality-parser.ts +97 -0
  76. package/src/lib/providers/openclaw.ts +17 -2
  77. package/src/lib/runtime-loop.ts +2 -2
  78. package/src/lib/server/chat-execution.ts +44 -2
  79. package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
  80. package/src/lib/server/connectors/bluebubbles.ts +5 -2
  81. package/src/lib/server/connectors/googlechat.ts +14 -10
  82. package/src/lib/server/connectors/manager.ts +37 -15
  83. package/src/lib/server/connectors/openclaw.ts +1 -0
  84. package/src/lib/server/daemon-state.ts +11 -0
  85. package/src/lib/server/main-agent-loop.ts +2 -3
  86. package/src/lib/server/main-session.ts +21 -0
  87. package/src/lib/server/openclaw-config-sync.ts +107 -0
  88. package/src/lib/server/openclaw-exec-config.ts +52 -0
  89. package/src/lib/server/openclaw-gateway.ts +291 -0
  90. package/src/lib/server/openclaw-history-merge.ts +36 -0
  91. package/src/lib/server/openclaw-models.ts +56 -0
  92. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  93. package/src/lib/server/openclaw-sync.ts +11 -10
  94. package/src/lib/server/queue.ts +2 -1
  95. package/src/lib/server/session-tools/connector.ts +4 -4
  96. package/src/lib/server/session-tools/delegate.ts +19 -3
  97. package/src/lib/server/session-tools/file.ts +20 -20
  98. package/src/lib/server/session-tools/index.ts +2 -2
  99. package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
  100. package/src/lib/server/session-tools/sandbox.ts +2 -2
  101. package/src/lib/server/session-tools/search-providers.ts +13 -6
  102. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  103. package/src/lib/server/session-tools/shell.ts +1 -1
  104. package/src/lib/server/session-tools/web.ts +8 -8
  105. package/src/lib/server/storage.ts +62 -11
  106. package/src/lib/server/stream-agent-chat.ts +24 -4
  107. package/src/lib/server/suggestions.ts +20 -0
  108. package/src/lib/server/ws-hub.ts +14 -0
  109. package/src/stores/use-app-store.ts +53 -1
  110. package/src/stores/use-approval-store.ts +78 -0
  111. package/src/stores/use-chat-store.ts +153 -5
  112. package/src/types/index.ts +127 -1
  113. package/tsconfig.json +13 -4
@@ -3,6 +3,7 @@ import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { enqueueSessionRun } from '@/lib/server/session-run-manager'
5
5
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
6
+ import { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main-session'
6
7
 
7
8
  function buildSessionAwakeningPrompt(user: string | null | undefined): string {
8
9
  const displayName = typeof user === 'string' && user.trim() ? user.trim() : 'there'
@@ -22,6 +23,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
22
23
  const updates = await req.json()
23
24
  const sessions = loadSessions()
24
25
  if (!sessions[id]) return notFound()
26
+ const wasProtectedMain = isProtectedMainSession(sessions[id])
25
27
  const hadMessagesBefore = Array.isArray(sessions[id].messages) && sessions[id].messages.length > 0
26
28
 
27
29
  const agentIdUpdateProvided = updates.agentId !== undefined
@@ -33,7 +35,13 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
33
35
 
34
36
  const linkedAgent = nextAgentId ? loadAgents()[nextAgentId] : null
35
37
 
36
- if (updates.name !== undefined) sessions[id].name = updates.name
38
+ if (updates.name !== undefined) {
39
+ const nextName = typeof updates.name === 'string' ? updates.name.trim() : String(updates.name || '')
40
+ if (wasProtectedMain && nextName !== '__main__') {
41
+ return new NextResponse('Cannot rename main chat session', { status: 400 })
42
+ }
43
+ sessions[id].name = updates.name
44
+ }
37
45
  if (updates.cwd !== undefined) sessions[id].cwd = updates.cwd
38
46
  if (updates.provider !== undefined) sessions[id].provider = updates.provider
39
47
  else if (agentIdUpdateProvided && linkedAgent?.provider) sessions[id].provider = linkedAgent.provider
@@ -62,8 +70,9 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
62
70
  if (updates.heartbeatIntervalSec !== undefined) sessions[id].heartbeatIntervalSec = updates.heartbeatIntervalSec
63
71
  if (updates.pinned !== undefined) sessions[id].pinned = !!updates.pinned
64
72
  if (!Array.isArray(sessions[id].messages)) sessions[id].messages = []
73
+ ensureMainSessionFlag(sessions[id])
65
74
 
66
- const shouldKickoffAwakening = sessions[id].name === '__main__'
75
+ const shouldKickoffAwakening = isProtectedMainSession(sessions[id])
67
76
  && agentIdUpdateProvided
68
77
  && !!sessions[id].agentId
69
78
  && !hadMessagesBefore
@@ -92,7 +101,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
92
101
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
93
102
  const { id } = await params
94
103
  const sessions = loadSessions()
95
- if (sessions[id]?.name === '__main__') {
104
+ if (isProtectedMainSession(sessions[id])) {
96
105
  return new NextResponse('Cannot delete main chat session', { status: 403 })
97
106
  }
98
107
  if (active.has(id)) {
@@ -7,6 +7,7 @@ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
7
7
  import { notify } from '@/lib/server/ws-hub'
8
8
  import { getSessionRunState } from '@/lib/server/session-run-manager'
9
9
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
10
+ import { ensureMainSessionFlag, isProtectedMainSession } from '@/lib/server/main-session'
10
11
  export const dynamic = 'force-dynamic'
11
12
 
12
13
 
@@ -27,16 +28,18 @@ export async function DELETE(req: Request) {
27
28
  return new NextResponse('Missing ids', { status: 400 })
28
29
  }
29
30
  const sessions = loadSessions()
31
+ let deleted = 0
30
32
  for (const id of ids) {
31
- if (sessions[id]?.name === '__main__') continue
33
+ if (isProtectedMainSession(sessions[id])) continue
32
34
  if (active.has(id)) {
33
35
  try { active.get(id).kill() } catch {}
34
36
  active.delete(id)
35
37
  }
36
38
  deleteSession(id)
39
+ deleted += 1
37
40
  }
38
41
  notify('sessions')
39
- return NextResponse.json({ deleted: ids.length })
42
+ return NextResponse.json({ deleted, requested: ids.length })
40
43
  }
41
44
 
42
45
  export async function POST(req: Request) {
@@ -86,6 +89,7 @@ export async function POST(req: Request) {
86
89
  heartbeatEnabled: body.heartbeatEnabled ?? null,
87
90
  heartbeatIntervalSec: body.heartbeatIntervalSec ?? null,
88
91
  }
92
+ ensureMainSessionFlag(sessions[id])
89
93
  saveSessions(sessions)
90
94
  notify('sessions')
91
95
  return NextResponse.json(sessions[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/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
  },
@@ -205,10 +208,36 @@ const COMMAND_GROUPS = [
205
208
  },
206
209
  {
207
210
  name: 'openclaw',
208
- description: 'OpenClaw discovery and sync',
211
+ description: 'OpenClaw discovery, gateway control, and runtime APIs',
209
212
  commands: [
210
213
  cmd('discover', 'GET', '/openclaw/discover', 'Discover OpenClaw gateways'),
211
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 }),
212
241
  cmd('sync', 'POST', '/openclaw/sync', 'Run OpenClaw sync action', { expectsJsonBody: true }),
213
242
  ],
214
243
  },
@@ -303,6 +332,9 @@ const COMMAND_GROUPS = [
303
332
  defaultBody: { action: 'disable_all' },
304
333
  }),
305
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 }),
306
338
  cmd('main-loop', 'GET', '/sessions/:id/main-loop', 'Get main mission loop state'),
307
339
  cmd('main-loop-action', 'POST', '/sessions/:id/main-loop', 'Control main mission loop (pause/resume/set_goal/set_mode/clear_events/nudge)', {
308
340
  expectsJsonBody: true,
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: {
@@ -130,6 +133,40 @@ const COMMAND_GROUPS = {
130
133
  graph: { description: 'Get orchestrator graph structure', method: 'GET', path: '/orchestrator/graph' },
131
134
  },
132
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' },
168
+ },
169
+ },
133
170
  plugins: {
134
171
  description: 'Plugin listing/config/install',
135
172
  commands: {
@@ -187,6 +224,9 @@ const COMMAND_GROUPS = {
187
224
  'delete-many': { description: 'Delete multiple sessions (body: {"ids":[...]})', method: 'DELETE', path: '/sessions' },
188
225
  'heartbeat-disable-all': { description: 'Disable all session heartbeats and cancel queued heartbeat runs', method: 'POST', path: '/sessions/heartbeat' },
189
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'] },
190
230
  'main-loop': { description: 'Get main mission loop state for a session', method: 'GET', path: '/sessions/:id/main-loop', params: ['id'] },
191
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'] },
192
232
  chat: { description: 'Send chat message (SSE stream)', method: 'POST', path: '/sessions/:id/chat', params: ['id'], stream: true, waitable: true },
@@ -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)}
@@ -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
+ }