@swarmclawai/swarmclaw 0.5.2 → 0.6.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 (173) hide show
  1. package/README.md +42 -7
  2. package/bin/swarmclaw.js +76 -16
  3. package/next.config.ts +11 -1
  4. package/package.json +4 -2
  5. package/public/screenshots/agents.png +0 -0
  6. package/public/screenshots/dashboard.png +0 -0
  7. package/public/screenshots/providers.png +0 -0
  8. package/public/screenshots/tasks.png +0 -0
  9. package/scripts/postinstall.mjs +18 -0
  10. package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
  11. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  12. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  13. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  14. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  15. package/src/app/api/chatrooms/route.ts +50 -0
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/knowledge/[id]/route.ts +13 -2
  18. package/src/app/api/knowledge/route.ts +8 -1
  19. package/src/app/api/memory/route.ts +8 -0
  20. package/src/app/api/notifications/[id]/route.ts +27 -0
  21. package/src/app/api/notifications/route.ts +68 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +155 -0
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/route.ts +3 -3
  29. package/src/app/api/settings/route.ts +9 -0
  30. package/src/app/api/setup/check-provider/route.ts +3 -16
  31. package/src/app/api/skills/[id]/route.ts +6 -0
  32. package/src/app/api/skills/route.ts +6 -0
  33. package/src/app/api/tasks/[id]/route.ts +20 -0
  34. package/src/app/api/tasks/bulk/route.ts +100 -0
  35. package/src/app/api/tasks/route.ts +1 -0
  36. package/src/app/api/usage/route.ts +45 -0
  37. package/src/app/api/webhooks/[id]/route.ts +15 -1
  38. package/src/app/globals.css +58 -15
  39. package/src/app/page.tsx +142 -13
  40. package/src/cli/index.js +42 -0
  41. package/src/cli/index.test.js +30 -0
  42. package/src/cli/spec.js +32 -0
  43. package/src/components/agents/agent-avatar.tsx +57 -10
  44. package/src/components/agents/agent-card.tsx +48 -15
  45. package/src/components/agents/agent-chat-list.tsx +123 -10
  46. package/src/components/agents/agent-list.tsx +50 -19
  47. package/src/components/agents/agent-sheet.tsx +56 -63
  48. package/src/components/auth/access-key-gate.tsx +10 -3
  49. package/src/components/auth/setup-wizard.tsx +2 -2
  50. package/src/components/auth/user-picker.tsx +31 -3
  51. package/src/components/chat/activity-moment.tsx +169 -0
  52. package/src/components/chat/chat-header.tsx +2 -0
  53. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  54. package/src/components/chat/file-path-chip.tsx +125 -0
  55. package/src/components/chat/markdown-utils.ts +9 -0
  56. package/src/components/chat/message-bubble.tsx +46 -295
  57. package/src/components/chat/message-list.tsx +50 -1
  58. package/src/components/chat/streaming-bubble.tsx +36 -46
  59. package/src/components/chat/suggestions-bar.tsx +1 -1
  60. package/src/components/chat/thinking-indicator.tsx +72 -10
  61. package/src/components/chat/tool-call-bubble.tsx +66 -70
  62. package/src/components/chat/tool-request-banner.tsx +31 -7
  63. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  64. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  65. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  66. package/src/components/chatrooms/chatroom-list.tsx +123 -0
  67. package/src/components/chatrooms/chatroom-message.tsx +427 -0
  68. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  69. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  70. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  71. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  72. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  73. package/src/components/connectors/connector-sheet.tsx +34 -47
  74. package/src/components/home/home-view.tsx +501 -0
  75. package/src/components/input/chat-input.tsx +79 -41
  76. package/src/components/knowledge/knowledge-list.tsx +31 -1
  77. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  78. package/src/components/layout/app-layout.tsx +209 -83
  79. package/src/components/layout/mobile-header.tsx +2 -0
  80. package/src/components/layout/update-banner.tsx +2 -2
  81. package/src/components/logs/log-list.tsx +2 -2
  82. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  83. package/src/components/memory/memory-agent-list.tsx +143 -0
  84. package/src/components/memory/memory-browser.tsx +205 -0
  85. package/src/components/memory/memory-card.tsx +34 -7
  86. package/src/components/memory/memory-detail.tsx +359 -120
  87. package/src/components/memory/memory-sheet.tsx +157 -23
  88. package/src/components/plugins/plugin-list.tsx +1 -1
  89. package/src/components/plugins/plugin-sheet.tsx +1 -1
  90. package/src/components/projects/project-detail.tsx +509 -0
  91. package/src/components/projects/project-list.tsx +195 -59
  92. package/src/components/providers/provider-list.tsx +2 -2
  93. package/src/components/providers/provider-sheet.tsx +3 -3
  94. package/src/components/schedules/schedule-card.tsx +3 -2
  95. package/src/components/schedules/schedule-list.tsx +1 -1
  96. package/src/components/schedules/schedule-sheet.tsx +25 -25
  97. package/src/components/secrets/secret-sheet.tsx +47 -24
  98. package/src/components/secrets/secrets-list.tsx +18 -8
  99. package/src/components/sessions/new-session-sheet.tsx +33 -65
  100. package/src/components/sessions/session-card.tsx +45 -14
  101. package/src/components/sessions/session-list.tsx +35 -18
  102. package/src/components/shared/agent-picker-list.tsx +90 -0
  103. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  104. package/src/components/shared/attachment-chip.tsx +165 -0
  105. package/src/components/shared/avatar.tsx +10 -1
  106. package/src/components/shared/check-icon.tsx +12 -0
  107. package/src/components/shared/confirm-dialog.tsx +1 -1
  108. package/src/components/shared/empty-state.tsx +32 -0
  109. package/src/components/shared/file-preview.tsx +34 -0
  110. package/src/components/shared/form-styles.ts +2 -0
  111. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  112. package/src/components/shared/notification-center.tsx +223 -0
  113. package/src/components/shared/profile-sheet.tsx +115 -0
  114. package/src/components/shared/reply-quote.tsx +26 -0
  115. package/src/components/shared/search-dialog.tsx +296 -0
  116. package/src/components/shared/section-label.tsx +12 -0
  117. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  118. package/src/components/shared/settings/section-providers.tsx +1 -1
  119. package/src/components/shared/settings/section-secrets.tsx +1 -1
  120. package/src/components/shared/settings/section-theme.tsx +95 -0
  121. package/src/components/shared/settings/section-user-preferences.tsx +39 -0
  122. package/src/components/shared/settings/settings-page.tsx +180 -27
  123. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  124. package/src/components/shared/sheet-footer.tsx +33 -0
  125. package/src/components/skills/skill-list.tsx +61 -30
  126. package/src/components/skills/skill-sheet.tsx +81 -2
  127. package/src/components/tasks/task-board.tsx +448 -26
  128. package/src/components/tasks/task-card.tsx +46 -9
  129. package/src/components/tasks/task-column.tsx +62 -3
  130. package/src/components/tasks/task-list.tsx +12 -4
  131. package/src/components/tasks/task-sheet.tsx +89 -72
  132. package/src/components/ui/hover-card.tsx +52 -0
  133. package/src/components/usage/metrics-dashboard.tsx +78 -0
  134. package/src/components/usage/usage-list.tsx +1 -1
  135. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  136. package/src/hooks/use-view-router.ts +69 -19
  137. package/src/instrumentation.ts +15 -1
  138. package/src/lib/chat.ts +2 -0
  139. package/src/lib/cron-human.ts +114 -0
  140. package/src/lib/memory.ts +3 -0
  141. package/src/lib/server/chat-execution.ts +24 -4
  142. package/src/lib/server/connectors/manager.ts +11 -0
  143. package/src/lib/server/context-manager.ts +225 -13
  144. package/src/lib/server/create-notification.ts +42 -0
  145. package/src/lib/server/daemon-state.ts +165 -10
  146. package/src/lib/server/execution-log.ts +1 -0
  147. package/src/lib/server/heartbeat-service.ts +40 -5
  148. package/src/lib/server/heartbeat-wake.ts +110 -0
  149. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  150. package/src/lib/server/memory-consolidation.ts +92 -0
  151. package/src/lib/server/memory-db.ts +51 -6
  152. package/src/lib/server/openclaw-gateway.ts +9 -1
  153. package/src/lib/server/provider-health.ts +125 -0
  154. package/src/lib/server/queue.ts +5 -4
  155. package/src/lib/server/scheduler.ts +8 -0
  156. package/src/lib/server/session-run-manager.ts +4 -0
  157. package/src/lib/server/session-tools/chatroom.ts +136 -0
  158. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  159. package/src/lib/server/session-tools/index.ts +2 -0
  160. package/src/lib/server/session-tools/memory.ts +6 -1
  161. package/src/lib/server/storage.ts +80 -29
  162. package/src/lib/server/stream-agent-chat.ts +153 -47
  163. package/src/lib/server/system-events.ts +49 -0
  164. package/src/lib/server/ws-hub.ts +11 -0
  165. package/src/lib/soul-suggestions.ts +109 -0
  166. package/src/lib/tasks.ts +4 -1
  167. package/src/lib/view-routes.ts +36 -1
  168. package/src/lib/ws-client.ts +14 -4
  169. package/src/proxy.ts +79 -2
  170. package/src/stores/use-app-store.ts +94 -3
  171. package/src/stores/use-chat-store.ts +48 -3
  172. package/src/stores/use-chatroom-store.ts +276 -0
  173. package/src/types/index.ts +69 -2
@@ -0,0 +1,100 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadTasks, saveTasks, logActivity } from '@/lib/server/storage'
3
+ import { enqueueTask, disableSessionHeartbeat } from '@/lib/server/queue'
4
+ import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
5
+ import { notify } from '@/lib/server/ws-hub'
6
+ import { createNotification } from '@/lib/server/create-notification'
7
+ import type { BoardTaskStatus } from '@/types'
8
+
9
+ const VALID_STATUSES: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed', 'archived']
10
+
11
+ /**
12
+ * Bulk update tasks — batch status changes, agent/project reassignment, or archive/delete.
13
+ *
14
+ * POST body:
15
+ * ids: string[] — required, task IDs to update
16
+ * status?: BoardTaskStatus — move all to this status
17
+ * agentId?: string | null — reassign agent (null to clear)
18
+ * projectId?: string | null — reassign project (null to clear)
19
+ */
20
+ export async function POST(req: Request) {
21
+ const body = await req.json()
22
+ const ids: unknown = body.ids
23
+ if (!Array.isArray(ids) || ids.length === 0) {
24
+ return NextResponse.json({ error: 'ids must be a non-empty array' }, { status: 400 })
25
+ }
26
+
27
+ const taskIds = ids.filter((id): id is string => typeof id === 'string')
28
+ if (taskIds.length === 0) {
29
+ return NextResponse.json({ error: 'No valid task IDs provided' }, { status: 400 })
30
+ }
31
+
32
+ const tasks = loadTasks()
33
+ let updated = 0
34
+ const results: string[] = []
35
+
36
+ for (const id of taskIds) {
37
+ if (!tasks[id]) continue
38
+ const prevStatus = tasks[id].status
39
+
40
+ if (typeof body.status === 'string' && VALID_STATUSES.includes(body.status as BoardTaskStatus)) {
41
+ tasks[id].status = body.status as BoardTaskStatus
42
+ if (body.status === 'archived' && prevStatus !== 'archived') {
43
+ tasks[id].archivedAt = Date.now()
44
+ }
45
+ }
46
+
47
+ if ('agentId' in body) {
48
+ tasks[id].agentId = body.agentId === null ? '' : String(body.agentId)
49
+ }
50
+
51
+ if ('projectId' in body) {
52
+ if (body.projectId === null) {
53
+ delete tasks[id].projectId
54
+ } else {
55
+ tasks[id].projectId = String(body.projectId)
56
+ }
57
+ }
58
+
59
+ tasks[id].updatedAt = Date.now()
60
+ updated++
61
+ results.push(id)
62
+
63
+ // Side-effects for status transitions
64
+ if (prevStatus !== tasks[id].status) {
65
+ logActivity({
66
+ entityType: 'task',
67
+ entityId: id,
68
+ action: 'updated',
69
+ actor: 'user',
70
+ summary: `Bulk update: "${tasks[id].title}" (${prevStatus} → ${tasks[id].status})`,
71
+ })
72
+ pushMainLoopEventToMainSessions({
73
+ type: 'task_status_changed',
74
+ text: `Task "${tasks[id].title}" (${id}) moved ${prevStatus} → ${tasks[id].status}.`,
75
+ })
76
+ if (tasks[id].status === 'completed' || tasks[id].status === 'failed') {
77
+ disableSessionHeartbeat(tasks[id].sessionId)
78
+ }
79
+ if (prevStatus !== 'queued' && tasks[id].status === 'queued') {
80
+ enqueueTask(id)
81
+ }
82
+ }
83
+ }
84
+
85
+ saveTasks(tasks)
86
+
87
+ if (updated > 0) {
88
+ const action = body.status
89
+ ? `moved ${updated} task(s) to ${body.status}`
90
+ : `updated ${updated} task(s)`
91
+ createNotification({
92
+ type: 'success',
93
+ title: `Bulk update: ${action}`,
94
+ entityType: 'task',
95
+ })
96
+ }
97
+
98
+ notify('tasks')
99
+ return NextResponse.json({ updated, ids: results })
100
+ }
@@ -70,6 +70,7 @@ export async function POST(req: Request) {
70
70
  description: body.description || '',
71
71
  status: body.status || 'backlog',
72
72
  agentId: body.agentId || '',
73
+ projectId: typeof body.projectId === 'string' && body.projectId ? body.projectId : null,
73
74
  goalContract: body.goalContract || null,
74
75
  cwd: typeof body.cwd === 'string' ? body.cwd : null,
75
76
  file: typeof body.file === 'string' ? body.file : null,
@@ -78,6 +78,50 @@ export async function GET(req: Request) {
78
78
  .sort(([a], [b]) => a.localeCompare(b))
79
79
  .map(([bucket, data]) => ({ bucket, tokens: data.tokens, cost: Math.round(data.cost * 10000) / 10000 }))
80
80
 
81
+ // Provider health stats
82
+ const healthAccum: Record<string, {
83
+ totalRequests: number
84
+ successCount: number
85
+ errorCount: number
86
+ lastUsed: number
87
+ models: Set<string>
88
+ }> = {}
89
+
90
+ for (const r of records) {
91
+ const prov = r.provider || 'unknown'
92
+ if (!healthAccum[prov]) {
93
+ healthAccum[prov] = { totalRequests: 0, successCount: 0, errorCount: 0, lastUsed: 0, models: new Set() }
94
+ }
95
+ const h = healthAccum[prov]
96
+ h.totalRequests += 1
97
+ // UsageRecord has no error/status field — logged records are successful completions
98
+ h.successCount += 1
99
+ if ((r.timestamp || 0) > h.lastUsed) h.lastUsed = r.timestamp || 0
100
+ if (r.model) h.models.add(r.model)
101
+ }
102
+
103
+ const providerHealth: Record<string, {
104
+ totalRequests: number
105
+ successCount: number
106
+ errorCount: number
107
+ errorRate: number
108
+ avgLatencyMs: number
109
+ lastUsed: number
110
+ models: string[]
111
+ }> = {}
112
+
113
+ for (const [prov, h] of Object.entries(healthAccum)) {
114
+ providerHealth[prov] = {
115
+ totalRequests: h.totalRequests,
116
+ successCount: h.successCount,
117
+ errorCount: h.errorCount,
118
+ errorRate: h.totalRequests > 0 ? h.errorCount / h.totalRequests : 0,
119
+ avgLatencyMs: 0, // UsageRecord does not track latency
120
+ lastUsed: h.lastUsed,
121
+ models: Array.from(h.models),
122
+ }
123
+ }
124
+
81
125
  return NextResponse.json({
82
126
  records,
83
127
  totalTokens,
@@ -85,5 +129,6 @@ export async function GET(req: Request) {
85
129
  byAgent,
86
130
  byProvider,
87
131
  timeSeries,
132
+ providerHealth,
88
133
  })
89
134
  }
@@ -1,8 +1,11 @@
1
1
  import { genId } from '@/lib/id'
2
+ import { timingSafeEqual } from 'node:crypto'
2
3
  import { NextResponse } from 'next/server'
3
4
  import { loadAgents, loadSessions, loadWebhooks, saveSessions, saveWebhooks, appendWebhookLog, upsertWebhookRetry } from '@/lib/server/storage'
4
5
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
6
  import { enqueueSessionRun } from '@/lib/server/session-run-manager'
7
+ import { enqueueSystemEvent } from '@/lib/server/system-events'
8
+ import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
6
9
  import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
7
10
  import type { WebhookRetryEntry } from '@/types'
8
11
 
@@ -71,7 +74,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
71
74
  if (secret) {
72
75
  const url = new URL(req.url)
73
76
  const provided = req.headers.get('x-webhook-secret') || url.searchParams.get('secret') || ''
74
- if (provided !== secret) {
77
+ const secretBuf = Buffer.from(secret)
78
+ const providedBuf = Buffer.from(provided)
79
+ // timingSafeEqual requires equal lengths; compare against secretBuf if lengths differ
80
+ const compareBuf = providedBuf.length === secretBuf.length ? providedBuf : secretBuf
81
+ const isInvalid = providedBuf.length !== secretBuf.length || !timingSafeEqual(secretBuf, compareBuf)
82
+ if (isInvalid) {
75
83
  appendWebhookLog(genId(8), {
76
84
  id: genId(8), webhookId: id, event: 'unknown',
77
85
  payload: '', status: 'error', error: 'Invalid webhook secret', timestamp: Date.now(),
@@ -193,6 +201,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
193
201
  mode: 'followup',
194
202
  })
195
203
 
204
+ // Enqueue system event + heartbeat wake
205
+ enqueueSystemEvent(sid, `Webhook received: ${webhook.name || id} (${incomingEvent})`)
206
+ if (webhook.agentId) {
207
+ requestHeartbeatNow({ agentId: webhook.agentId, reason: 'webhook' })
208
+ }
209
+
196
210
  appendWebhookLog(genId(8), {
197
211
  id: genId(8), webhookId: id, event: incomingEvent,
198
212
  payload: (rawBody || '').slice(0, 2000), status: 'success',
@@ -49,16 +49,17 @@
49
49
  --radius-4xl: calc(var(--radius) + 16px);
50
50
 
51
51
  /* ===== Midnight Glass Palette ===== */
52
- --color-bg: #08080d;
53
- --color-raised: #0d0d14;
54
- --color-surface: #13131e;
55
- --color-surface-2: #1e1e30;
56
- --color-surface-3: #23233a;
52
+ /* Surfaces derived from --neutral-tint for single-knob theming */
53
+ --color-bg: color-mix(in srgb, var(--neutral-tint, #1e1e30) 40%, #000);
54
+ --color-raised: color-mix(in srgb, var(--neutral-tint, #1e1e30) 50%, #000);
55
+ --color-surface: color-mix(in srgb, var(--neutral-tint, #1e1e30) 65%, #000);
56
+ --color-surface-2: var(--neutral-tint, #1e1e30);
57
+ --color-surface-3: color-mix(in srgb, var(--neutral-tint, #1e1e30) 80%, #333);
57
58
  --color-border-hi: rgba(255,255,255,0.07);
58
59
  --color-border-focus: rgba(99,102,241,0.5);
59
60
  --color-text: #e2e2ec;
60
- --color-text-2: #8e8ea8;
61
- --color-text-3: #5c5c78;
61
+ --color-text-2: #a0a0ba;
62
+ --color-text-3: #8282a0;
62
63
  --color-accent-soft: rgba(99,102,241,0.08);
63
64
  --color-accent-glow: rgba(99,102,241,0.18);
64
65
  --color-accent-bright: #818CF8;
@@ -83,25 +84,29 @@
83
84
  --font-sora: 'Segoe UI';
84
85
  --font-jetbrains-mono: 'SF Mono';
85
86
  --radius: 0.625rem;
86
- --background: #08080d;
87
+
88
+ /* ===== Single-Tint Theming ===== */
89
+ /* Change this one value to shift the entire palette hue */
90
+ --neutral-tint: #1e1e30;
91
+ --background: color-mix(in srgb, var(--neutral-tint) 40%, #000);
87
92
  --foreground: #e2e2ec;
88
- --card: #0d0d14;
93
+ --card: color-mix(in srgb, var(--neutral-tint) 50%, #000);
89
94
  --card-foreground: #e2e2ec;
90
- --popover: #0d0d14;
95
+ --popover: color-mix(in srgb, var(--neutral-tint) 50%, #000);
91
96
  --popover-foreground: #e2e2ec;
92
97
  --primary: #6366F1;
93
98
  --primary-foreground: #ffffff;
94
- --secondary: #13131e;
99
+ --secondary: color-mix(in srgb, var(--neutral-tint) 65%, #000);
95
100
  --secondary-foreground: #e2e2ec;
96
- --muted: #13131e;
97
- --muted-foreground: #8e8ea8;
101
+ --muted: color-mix(in srgb, var(--neutral-tint) 65%, #000);
102
+ --muted-foreground: #a0a0ba;
98
103
  --accent: #6366F1;
99
104
  --accent-foreground: #ffffff;
100
105
  --destructive: #F43F5E;
101
106
  --border: rgba(255,255,255,0.04);
102
107
  --input: rgba(255,255,255,0.04);
103
108
  --ring: rgba(99,102,241,0.4);
104
- --sidebar: #0d0d14;
109
+ --sidebar: color-mix(in srgb, var(--neutral-tint) 50%, #000);
105
110
  --sidebar-foreground: #e2e2ec;
106
111
  --sidebar-primary: #6366F1;
107
112
  --sidebar-primary-foreground: #ffffff;
@@ -113,7 +118,7 @@
113
118
  /* ===== Status Badge System ===== */
114
119
  --status-idle-bg: rgba(255,255,255,0.04);
115
120
  --status-idle-border: rgba(255,255,255,0.06);
116
- --status-idle-fg: #8e8ea8;
121
+ --status-idle-fg: #a0a0ba;
117
122
  --status-running-bg: rgba(52,211,153,0.08);
118
123
  --status-running-border: rgba(52,211,153,0.15);
119
124
  --status-running-fg: #34D399;
@@ -205,6 +210,10 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
205
210
  from { opacity: 0; transform: scale(0.97) translateY(6px); }
206
211
  to { opacity: 1; transform: scale(1) translateY(0); }
207
212
  }
213
+ @keyframes msg-in {
214
+ from { opacity: 0; transform: translateY(6px); }
215
+ to { opacity: 1; transform: translateY(0); }
216
+ }
208
217
  @keyframes spin { to { transform: rotate(360deg); } }
209
218
  @keyframes slide-in-left {
210
219
  from { transform: translateX(-100%); }
@@ -243,6 +252,30 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
243
252
  to { opacity: 1; transform: translateY(0); }
244
253
  }
245
254
 
255
+ /* Heartbeat float animation */
256
+ @keyframes heartbeat-float {
257
+ 0% { opacity: 1; transform: translateY(0) scale(1); }
258
+ 50% { opacity: 0.7; transform: translateY(-14px) scale(1.15); }
259
+ 100% { opacity: 0; transform: translateY(-26px) scale(0.85); }
260
+ }
261
+
262
+ /* Activity moment animations */
263
+ @keyframes activity-moment-in {
264
+ from { opacity: 0; transform: translateY(6px) scale(0.92); }
265
+ to { opacity: 1; transform: translateY(0) scale(1); }
266
+ }
267
+ @keyframes activity-moment-out {
268
+ from { opacity: 1; transform: translateY(0) scale(1); }
269
+ to { opacity: 0; transform: translateY(-8px) scale(0.95); }
270
+ }
271
+
272
+ @keyframes avatar-moment-pulse {
273
+ 0% { transform: scale(1); }
274
+ 30% { transform: scale(1.15); }
275
+ 60% { transform: scale(0.95); }
276
+ 100% { transform: scale(1); }
277
+ }
278
+
246
279
  /* AI avatar mood animations */
247
280
  @keyframes ai-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.15); } }
248
281
  @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); } }
@@ -414,6 +447,16 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
414
447
  font-family: var(--font-sora), 'Sora', system-ui, sans-serif;
415
448
  }
416
449
 
450
+ /* Monospace micro-label utility */
451
+ .label-mono {
452
+ font-family: var(--font-mono);
453
+ font-size: 10px;
454
+ font-weight: 600;
455
+ letter-spacing: 0.06em;
456
+ text-transform: uppercase;
457
+ color: var(--color-text-3);
458
+ }
459
+
417
460
  /* ===== Status Badge Classes ===== */
418
461
  .status-badge-idle, .status-badge-running, .status-badge-error,
419
462
  .status-badge-connecting, .status-badge-connected, .status-badge-disconnected,
package/src/app/page.tsx CHANGED
@@ -12,6 +12,130 @@ import { SetupWizard } from '@/components/auth/setup-wizard'
12
12
  import { AppLayout } from '@/components/layout/app-layout'
13
13
  import { useViewRouter } from '@/hooks/use-view-router'
14
14
 
15
+ function FullScreenLoader() {
16
+ return (
17
+ <div className="h-full flex flex-col items-center justify-center bg-bg overflow-hidden select-none">
18
+ {/* Animated orbital ring */}
19
+ <div className="relative w-[120px] h-[120px] mb-8">
20
+ {/* Outer glow pulse */}
21
+ <div
22
+ className="absolute inset-[-20px] rounded-full"
23
+ style={{
24
+ background: 'radial-gradient(circle, rgba(99,102,241,0.08) 0%, transparent 70%)',
25
+ animation: 'sc-glow 2.5s ease-in-out infinite',
26
+ }}
27
+ />
28
+
29
+ {/* Orbital ring */}
30
+ <div
31
+ className="absolute inset-0 rounded-full border border-white/[0.06]"
32
+ style={{ animation: 'sc-ring 3s linear infinite' }}
33
+ />
34
+
35
+ {/* Orbiting dots */}
36
+ {[0, 1, 2, 3, 4, 5].map((i) => (
37
+ <div
38
+ key={i}
39
+ className="absolute inset-0"
40
+ style={{
41
+ animation: `sc-orbit 2.4s cubic-bezier(0.4, 0, 0.2, 1) infinite`,
42
+ animationDelay: `${i * -0.4}s`,
43
+ }}
44
+ >
45
+ <div
46
+ className="absolute top-0 left-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full"
47
+ style={{
48
+ width: i === 0 ? 8 : 6,
49
+ height: i === 0 ? 8 : 6,
50
+ background: i === 0 ? '#818CF8' : `rgba(129, 140, 248, ${0.7 - i * 0.1})`,
51
+ boxShadow: i === 0 ? '0 0 12px rgba(99,102,241,0.5)' : 'none',
52
+ }}
53
+ />
54
+ </div>
55
+ ))}
56
+
57
+ {/* Center logo mark */}
58
+ <div className="absolute inset-0 flex items-center justify-center">
59
+ <div
60
+ className="relative"
61
+ style={{ animation: 'sc-breathe 2.5s ease-in-out infinite' }}
62
+ >
63
+ <svg width="36" height="36" viewBox="0 0 36 36" fill="none">
64
+ {/* Hexagonal claw mark */}
65
+ <path
66
+ d="M18 4L30 11V25L18 32L6 25V11L18 4Z"
67
+ stroke="rgba(129, 140, 248, 0.3)"
68
+ strokeWidth="1"
69
+ fill="none"
70
+ />
71
+ <path
72
+ d="M18 9L25 13V23L18 27L11 23V13L18 9Z"
73
+ stroke="rgba(129, 140, 248, 0.5)"
74
+ strokeWidth="1.5"
75
+ fill="rgba(99, 102, 241, 0.06)"
76
+ />
77
+ {/* Claw lines */}
78
+ <path d="M14 15L18 20L22 15" stroke="#818CF8" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
79
+ <path d="M12 13L18 20L24 13" stroke="rgba(129, 140, 248, 0.3)" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
80
+ </svg>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ {/* Brand text */}
86
+ <div
87
+ className="text-[15px] font-display font-700 tracking-[0.15em] uppercase"
88
+ style={{
89
+ background: 'linear-gradient(135deg, rgba(255,255,255,0.6), rgba(129, 140, 248, 0.8))',
90
+ WebkitBackgroundClip: 'text',
91
+ WebkitTextFillColor: 'transparent',
92
+ animation: 'sc-text-fade 2s ease-in-out infinite alternate',
93
+ }}
94
+ >
95
+ SwarmClaw
96
+ </div>
97
+
98
+ {/* Loading bar */}
99
+ <div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden">
100
+ <div
101
+ className="h-full rounded-full bg-accent-bright/60"
102
+ style={{ animation: 'sc-progress 1.5s ease-in-out infinite' }}
103
+ />
104
+ </div>
105
+
106
+ {/* Loading animation keyframes */}
107
+ <style>{`
108
+ @keyframes sc-orbit {
109
+ from { transform: rotate(0deg); }
110
+ to { transform: rotate(360deg); }
111
+ }
112
+ @keyframes sc-ring {
113
+ from { transform: rotate(0deg) scale(1); }
114
+ 50% { transform: rotate(180deg) scale(1.02); }
115
+ to { transform: rotate(360deg) scale(1); }
116
+ }
117
+ @keyframes sc-breathe {
118
+ 0%, 100% { transform: scale(1); opacity: 0.9; }
119
+ 50% { transform: scale(1.06); opacity: 1; }
120
+ }
121
+ @keyframes sc-glow {
122
+ 0%, 100% { opacity: 0.5; transform: scale(0.9); }
123
+ 50% { opacity: 1; transform: scale(1.1); }
124
+ }
125
+ @keyframes sc-text-fade {
126
+ 0% { opacity: 0.6; }
127
+ 100% { opacity: 1; }
128
+ }
129
+ @keyframes sc-progress {
130
+ 0% { width: 0; margin-left: 0; }
131
+ 50% { width: 70%; margin-left: 15%; }
132
+ 100% { width: 0; margin-left: 100%; }
133
+ }
134
+ `}</style>
135
+ </div>
136
+ )
137
+ }
138
+
15
139
  export default function Home() {
16
140
  const currentUser = useAppStore((s) => s.currentUser)
17
141
  const setUser = useAppStore((s) => s.setUser)
@@ -83,26 +207,31 @@ export default function Home() {
83
207
 
84
208
  useWs('sessions', loadSessions, 5000)
85
209
 
86
- // Auto-select default agent's thread on load
210
+ // Auto-select agent's thread on load — resolves a persisted agentId into a session,
211
+ // or falls back to defaultAgentId from settings, then first agent.
212
+ const [agentReady, setAgentReady] = useState(false)
87
213
  useEffect(() => {
88
214
  if (!authenticated || !currentUser) return
89
- const state = useAppStore.getState()
90
- // Only auto-select if no agent is selected yet
91
- if (state.currentAgentId) return
92
-
93
- // Load agents and select 'default' agent
94
215
  let cancelled = false
95
216
  ;(async () => {
96
217
  try {
218
+ const state = useAppStore.getState()
97
219
  await state.loadAgents()
98
220
  if (cancelled) return
99
- const agents = useAppStore.getState().agents
100
- // Try 'default' agent first, then fall back to first agent
101
- const defaultAgent = agents['default'] || Object.values(agents)[0]
102
- if (defaultAgent) {
103
- await useAppStore.getState().setCurrentAgent(defaultAgent.id)
221
+
222
+ const { agents, currentAgentId, appSettings } = useAppStore.getState()
223
+ // Priority: persisted agent > settings default > first agent
224
+ const targetId = (currentAgentId && agents[currentAgentId])
225
+ ? currentAgentId
226
+ : (appSettings.defaultAgentId && agents[appSettings.defaultAgentId])
227
+ ? appSettings.defaultAgentId
228
+ : Object.values(agents)[0]?.id || null
229
+
230
+ if (targetId) {
231
+ await useAppStore.getState().setCurrentAgent(targetId)
104
232
  }
105
233
  } catch { /* ignore */ }
234
+ if (!cancelled) setAgentReady(true)
106
235
  })()
107
236
  return () => { cancelled = true }
108
237
  }, [authenticated, currentUser])
@@ -148,10 +277,10 @@ export default function Home() {
148
277
 
149
278
  useViewRouter()
150
279
 
151
- if (!hydrated || !authChecked) return null
280
+ if (!hydrated || !authChecked) return <FullScreenLoader />
152
281
  if (!authenticated) return <AccessKeyGate onAuthenticated={() => setAuthenticated(true)} />
153
282
  if (!currentUser) return <UserPicker />
154
- if (setupDone === null) return null
283
+ if (setupDone === null || !agentReady) return <FullScreenLoader />
155
284
  if (!setupDone) return <SetupWizard onComplete={() => setSetupDone(true)} />
156
285
  return <AppLayout />
157
286
  }
package/src/cli/index.js CHANGED
@@ -56,6 +56,29 @@ const COMMAND_GROUPS = [
56
56
  cmd('install', 'POST', '/clawhub/install', 'Install a skill from ClawHub', { expectsJsonBody: true }),
57
57
  ],
58
58
  },
59
+ {
60
+ name: 'chatrooms',
61
+ description: 'Manage multi-agent chatrooms',
62
+ commands: [
63
+ cmd('list', 'GET', '/chatrooms', 'List chatrooms'),
64
+ cmd('get', 'GET', '/chatrooms/:id', 'Get chatroom by id'),
65
+ cmd('create', 'POST', '/chatrooms', 'Create a chatroom', { expectsJsonBody: true }),
66
+ cmd('update', 'PUT', '/chatrooms/:id', 'Update a chatroom', { expectsJsonBody: true }),
67
+ cmd('delete', 'DELETE', '/chatrooms/:id', 'Delete a chatroom'),
68
+ cmd('chat', 'POST', '/chatrooms/:id/chat', 'Post a message to a chatroom and stream agent replies', {
69
+ expectsJsonBody: true,
70
+ responseType: 'sse',
71
+ }),
72
+ cmd('add-member', 'POST', '/chatrooms/:id/members', 'Add an agent to a chatroom (use --data \'{"agentId":"..."}\')', { expectsJsonBody: true }),
73
+ cmd('remove-member', 'DELETE', '/chatrooms/:id/members', 'Remove an agent from a chatroom (use --data \'{"agentId":"..."}\')', { expectsJsonBody: true }),
74
+ cmd('react', 'POST', '/chatrooms/:id/reactions', 'Toggle a reaction on a chatroom message', {
75
+ expectsJsonBody: true,
76
+ }),
77
+ cmd('pin', 'POST', '/chatrooms/:id/pins', 'Toggle pin on a chatroom message', {
78
+ expectsJsonBody: true,
79
+ }),
80
+ ],
81
+ },
59
82
  {
60
83
  name: 'connectors',
61
84
  description: 'Manage chat connectors',
@@ -183,6 +206,17 @@ const COMMAND_GROUPS = [
183
206
  cmd('get', 'GET', '/memory-images/:filename', 'Download memory image by filename', { responseType: 'binary' }),
184
207
  ],
185
208
  },
209
+ {
210
+ name: 'notifications',
211
+ description: 'Manage in-app notifications',
212
+ commands: [
213
+ cmd('list', 'GET', '/notifications', 'List notifications (use --query unreadOnly=true --query limit=100)'),
214
+ cmd('create', 'POST', '/notifications', 'Create notification', { expectsJsonBody: true }),
215
+ cmd('clear', 'DELETE', '/notifications', 'Clear read notifications'),
216
+ cmd('mark-read', 'PUT', '/notifications/:id', 'Mark notification as read'),
217
+ cmd('delete', 'DELETE', '/notifications/:id', 'Delete notification by id'),
218
+ ],
219
+ },
186
220
  {
187
221
  name: 'mcp-servers',
188
222
  description: 'Manage MCP server configurations',
@@ -293,6 +327,13 @@ const COMMAND_GROUPS = [
293
327
  cmd('models-clear', 'DELETE', '/providers/:id/models', 'Clear provider model overrides'),
294
328
  ],
295
329
  },
330
+ {
331
+ name: 'search',
332
+ description: 'Global search across app resources',
333
+ commands: [
334
+ cmd('query', 'GET', '/search', 'Search agents/tasks/sessions/schedules/webhooks/skills (use --query q=term)'),
335
+ ],
336
+ },
296
337
  {
297
338
  name: 'runs',
298
339
  description: 'Session run queue/history',
@@ -409,6 +450,7 @@ const COMMAND_GROUPS = [
409
450
  cmd('list', 'GET', '/tasks', 'List tasks'),
410
451
  cmd('get', 'GET', '/tasks/:id', 'Get task'),
411
452
  cmd('create', 'POST', '/tasks', 'Create task', { expectsJsonBody: true }),
453
+ cmd('bulk', 'POST', '/tasks/bulk', 'Bulk update tasks (status/agent/project)', { expectsJsonBody: true }),
412
454
  cmd('update', 'PUT', '/tasks/:id', 'Update task', { expectsJsonBody: true }),
413
455
  cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'),
414
456
  cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }),
@@ -81,6 +81,36 @@ test('CLI command map covers all API route method/path pairs', () => {
81
81
  assert.deepEqual(missing, [])
82
82
  })
83
83
 
84
+ test('Binary CLI router reaches every mapped API command pair', async () => {
85
+ const { shouldUseLegacyTsCli, TS_CLI_ACTIONS } = await import('../../bin/swarmclaw.js')
86
+
87
+ for (const command of COMMANDS) {
88
+ if (command.virtual) continue
89
+
90
+ const pathArgs = extractPathParams(command.route).map((name, index) => `${name}-${index + 1}`)
91
+ const routedToLegacyTs = shouldUseLegacyTsCli([command.group, command.action, ...pathArgs])
92
+
93
+ if (routedToLegacyTs) {
94
+ assert.ok(
95
+ TS_CLI_ACTIONS[command.group]?.has(command.action),
96
+ `legacy TS router should only claim known actions (${command.group} ${command.action})`,
97
+ )
98
+ }
99
+ }
100
+
101
+ // Spot-check known API commands that are map-only today.
102
+ assert.equal(shouldUseLegacyTsCli(['chatrooms', 'list']), false)
103
+ assert.equal(shouldUseLegacyTsCli(['tasks', 'approve', 'task-1']), false)
104
+
105
+ // Help paths should route to mapped CLI for full command discoverability.
106
+ assert.equal(shouldUseLegacyTsCli([]), false)
107
+ assert.equal(shouldUseLegacyTsCli(['--help']), false)
108
+ assert.equal(shouldUseLegacyTsCli(['tasks', '--help']), false)
109
+
110
+ // And a legacy command that should remain on the richer TS path.
111
+ assert.equal(shouldUseLegacyTsCli(['tasks', 'create']), true)
112
+ })
113
+
84
114
  test('parseArgv parses group/action/options', () => {
85
115
  const parsed = parseArgv([
86
116
  'runs',