@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -197,12 +197,120 @@ export function HomeView() {
197
197
  SwarmClaw
198
198
  </h1>
199
199
  <p className="text-[14px] text-text-3 mt-1">
200
- Your AI agent orchestration dashboard
200
+ Workspace overview for your agent chats, tasks, and automations
201
201
  </p>
202
202
  </div>
203
203
 
204
+ {/* Quick actions / triage */}
205
+ <section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.15s both' }}>
206
+ <SectionHeader
207
+ label="Needs Attention"
208
+ actionLabel="Open Tasks"
209
+ onViewAll={activeTaskCount > 0 ? () => setActiveView('tasks') : undefined}
210
+ />
211
+ <div className="rounded-[18px] border border-white/[0.06] bg-white/[0.025] p-4">
212
+ <div className="flex flex-wrap items-center gap-2 mb-4">
213
+ <StatusPill label={`${allTasks.filter((task) => task.status === 'failed').length} failed task${allTasks.filter((task) => task.status === 'failed').length === 1 ? '' : 's'}`} tone={allTasks.some((task) => task.status === 'failed') ? 'danger' : 'neutral'} />
214
+ <StatusPill label={`${allTasks.filter((task) => task.pendingApproval).length} awaiting approval`} tone={allTasks.some((task) => task.pendingApproval) ? 'warning' : 'neutral'} />
215
+ <StatusPill label={`${allConnectors.filter((connector) => connector.status === 'error').length} connector issue${allConnectors.filter((connector) => connector.status === 'error').length === 1 ? '' : 's'}`} tone={allConnectors.some((connector) => connector.status === 'error') ? 'danger' : 'neutral'} />
216
+ <StatusPill label={`${runningAgentIds.size} active agent${runningAgentIds.size === 1 ? '' : 's'}`} tone={runningAgentIds.size > 0 ? 'success' : 'neutral'} />
217
+ </div>
218
+
219
+ {(() => {
220
+ const now = Date.now()
221
+ const items = [
222
+ ...allTasks
223
+ .filter((task) => task.pendingApproval)
224
+ .slice(0, 2)
225
+ .map((task) => ({
226
+ id: `approval:${task.id}`,
227
+ tone: 'warning' as const,
228
+ label: task.title,
229
+ meta: `${task.agentId && agents[task.agentId] ? agents[task.agentId]!.name : 'Task'} is waiting for approval`,
230
+ onClick: () => handleTaskClick(task),
231
+ })),
232
+ ...allTasks
233
+ .filter((task) => task.status === 'failed')
234
+ .slice(0, 2)
235
+ .map((task) => ({
236
+ id: `failed:${task.id}`,
237
+ tone: 'danger' as const,
238
+ label: task.title,
239
+ meta: `Failed ${timeAgo(task.updatedAt || task.createdAt)}`,
240
+ onClick: () => handleTaskClick(task),
241
+ })),
242
+ ...allConnectors
243
+ .filter((connector) => connector.status === 'error')
244
+ .slice(0, 2)
245
+ .map((connector) => ({
246
+ id: `connector:${connector.id}`,
247
+ tone: 'danger' as const,
248
+ label: connector.name,
249
+ meta: `${PLATFORM_LABELS[connector.platform] || connector.platform} connector needs attention`,
250
+ onClick: () => setActiveView('connectors'),
251
+ })),
252
+ ...Object.values(schedules)
253
+ .filter((schedule) => schedule.status === 'active' && schedule.nextRunAt && schedule.nextRunAt < now)
254
+ .slice(0, 2)
255
+ .map((schedule) => ({
256
+ id: `schedule:${schedule.id}`,
257
+ tone: 'warning' as const,
258
+ label: schedule.name,
259
+ meta: 'Schedule missed its expected run window',
260
+ onClick: () => setActiveView('schedules'),
261
+ })),
262
+ ...unreadNotifications
263
+ .slice(0, 2)
264
+ .map((notification) => ({
265
+ id: `notification:${notification.id}`,
266
+ tone: 'info' as const,
267
+ label: notification.title,
268
+ meta: notification.message || 'Unread notification',
269
+ onClick: () => handleNotificationClick(notification),
270
+ })),
271
+ ].slice(0, 6)
272
+
273
+ if (items.length === 0) {
274
+ return (
275
+ <div className="rounded-[14px] border border-dashed border-white/[0.06] bg-white/[0.02] px-4 py-5">
276
+ <p className="text-[13px] font-600 text-text">Everything looks stable.</p>
277
+ <p className="text-[12px] text-text-3/60 mt-1">
278
+ No failed tasks, no waiting approvals, and no connector issues right now.
279
+ </p>
280
+ </div>
281
+ )
282
+ }
283
+
284
+ return (
285
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
286
+ {items.map((item) => (
287
+ <button
288
+ key={item.id}
289
+ onClick={item.onClick}
290
+ className="flex items-start gap-3 rounded-[14px] border border-white/[0.06] bg-transparent px-4 py-3 text-left hover:bg-white/[0.04] transition-colors cursor-pointer"
291
+ style={{ fontFamily: 'inherit' }}
292
+ >
293
+ <div className={`mt-0.5 h-2.5 w-2.5 rounded-full shrink-0 ${
294
+ item.tone === 'danger'
295
+ ? 'bg-red-400'
296
+ : item.tone === 'warning'
297
+ ? 'bg-amber-400'
298
+ : 'bg-sky-400'
299
+ }`} />
300
+ <div className="min-w-0">
301
+ <div className="text-[13px] font-600 text-text truncate">{item.label}</div>
302
+ <div className="text-[11px] text-text-3/65 mt-1">{item.meta}</div>
303
+ </div>
304
+ </button>
305
+ ))}
306
+ </div>
307
+ )
308
+ })()}
309
+ </div>
310
+ </section>
311
+
204
312
  {/* Quick Stats */}
205
- <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
313
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-8">
206
314
  <StatCard label="Agents" value={String(agentCount)} hint="Total active agents configured in your dashboard" index={0} />
207
315
  <StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} hint="Tasks currently running or queued for execution" index={1} />
208
316
  <StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} hint="Estimated API cost for today across all providers" index={2} />
@@ -507,7 +615,7 @@ export function HomeView() {
507
615
  )
508
616
  }
509
617
 
510
- function SectionHeader({ label, onViewAll }: { label: string; onViewAll?: () => void }) {
618
+ function SectionHeader({ label, onViewAll, actionLabel = 'View all →' }: { label: string; onViewAll?: () => void; actionLabel?: string }) {
511
619
  return (
512
620
  <div className="flex items-center justify-between mb-3">
513
621
  <h2 className="font-display text-[13px] font-600 text-text-2 uppercase tracking-[0.08em]">
@@ -519,13 +627,29 @@ function SectionHeader({ label, onViewAll }: { label: string; onViewAll?: () =>
519
627
  className="text-[11px] text-text-3/50 hover:text-text-3 transition-colors bg-transparent border-none cursor-pointer"
520
628
  style={{ fontFamily: 'inherit' }}
521
629
  >
522
- View all →
630
+ {actionLabel}
523
631
  </button>
524
632
  )}
525
633
  </div>
526
634
  )
527
635
  }
528
636
 
637
+ function StatusPill({ label, tone }: { label: string; tone: 'neutral' | 'warning' | 'danger' | 'success' }) {
638
+ const toneClasses = tone === 'danger'
639
+ ? 'border-red-400/20 bg-red-400/[0.05] text-red-300/85'
640
+ : tone === 'warning'
641
+ ? 'border-amber-400/20 bg-amber-400/[0.05] text-amber-300/85'
642
+ : tone === 'success'
643
+ ? 'border-emerald-400/20 bg-emerald-400/[0.05] text-emerald-300/85'
644
+ : 'border-white/[0.06] bg-white/[0.03] text-text-3/75'
645
+
646
+ return (
647
+ <div className={`rounded-[999px] border px-3 py-1.5 text-[11px] font-600 ${toneClasses}`}>
648
+ {label}
649
+ </div>
650
+ )
651
+ }
652
+
529
653
  function StatCard({ label, value, accent, hint, index = 0 }: { label: string; value: string; accent?: boolean; hint?: string; index?: number }) {
530
654
  return (
531
655
  <div
@@ -24,9 +24,11 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
24
24
 
25
25
  export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }: Props) {
26
26
  const [value, setValue] = useState('')
27
+ const [extrasOpen, setExtrasOpen] = useState(false)
27
28
  const { ref: textareaRef, resize } = useAutoResize()
28
29
  const fileInputRef = useRef<HTMLInputElement>(null)
29
30
  const imageInputRef = useRef<HTMLInputElement>(null)
31
+ const extrasRef = useRef<HTMLDivElement>(null)
30
32
  const pendingFiles = useChatStore((s) => s.pendingFiles)
31
33
  const addPendingFile = useChatStore((s) => s.addPendingFile)
32
34
  const removePendingFile = useChatStore((s) => s.removePendingFile)
@@ -37,6 +39,17 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
37
39
  const addQueuedMessage = useChatStore((s) => s.addQueuedMessage)
38
40
  const removeQueuedMessage = useChatStore((s) => s.removeQueuedMessage)
39
41
 
42
+ useEffect(() => {
43
+ if (!extrasOpen) return
44
+ const handler = (e: MouseEvent) => {
45
+ if (extrasRef.current && !extrasRef.current.contains(e.target as Node)) {
46
+ setExtrasOpen(false)
47
+ }
48
+ }
49
+ document.addEventListener('mousedown', handler)
50
+ return () => document.removeEventListener('mousedown', handler)
51
+ }, [extrasOpen])
52
+
40
53
  // Draft persistence: restore on session change
41
54
  const draftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
42
55
  useEffect(() => {
@@ -61,6 +74,10 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
61
74
  if (!text && !pendingFiles.length) return
62
75
  // If streaming, queue the message instead of blocking
63
76
  if (streaming) {
77
+ if (pendingFiles.length > 0) {
78
+ toast.error('Wait for the current reply to finish before sending files.')
79
+ return
80
+ }
64
81
  if (text) {
65
82
  addQueuedMessage(text)
66
83
  setValue('')
@@ -133,24 +150,30 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
133
150
  return (
134
151
  <div className="shrink-0 px-4 md:px-12 lg:px-16 pb-4 pt-2 fixed bottom-0 left-0 right-0 z-20 bg-bg/95 backdrop-blur-md md:relative md:z-auto md:bg-transparent md:backdrop-blur-none"
135
152
  style={{ paddingBottom: 'max(16px, env(safe-area-inset-bottom))' }}>
136
- <div>
153
+ <div className="relative" ref={extrasRef}>
137
154
  {streaming && (
138
- <div className="flex justify-center py-2 mb-2">
155
+ <div className="mb-2 flex flex-wrap items-center justify-between gap-2 rounded-[14px] border border-amber-500/15 bg-amber-500/[0.06] px-3.5 py-2">
156
+ <div className="min-w-0">
157
+ <div className="text-[12px] font-600 text-amber-300">Reply in progress</div>
158
+ <div className="text-[11px] text-amber-200/70">
159
+ New text sends queue automatically. File uploads wait for the current reply to finish.
160
+ </div>
161
+ </div>
139
162
  <button
140
163
  onClick={onStop}
141
- className="px-6 py-2.5 rounded-pill border border-danger/20 bg-danger/[0.06]
142
- text-danger text-[13px] font-600 cursor-pointer transition-all duration-200
143
- active:scale-95 hover:bg-danger/[0.1] hover:border-danger/30"
164
+ className="px-4 py-2 rounded-pill border border-danger/20 bg-danger/[0.06]
165
+ text-danger text-[12px] font-600 cursor-pointer transition-all duration-200
166
+ active:scale-95 hover:bg-danger/[0.1] hover:border-danger/30 shrink-0"
144
167
  style={{ fontFamily: 'inherit' }}
145
168
  >
146
- Stop generating
169
+ Stop
147
170
  </button>
148
171
  </div>
149
172
  )}
150
173
 
151
174
  {queuedMessages.length > 0 && (
152
175
  <div className="flex flex-wrap items-center gap-1.5 mb-2">
153
- <span className="label-mono text-amber-400/70">Queued</span>
176
+ <span className="label-mono text-amber-400/70">Sending next</span>
154
177
  {queuedMessages.map((msg, i) => (
155
178
  <span key={i} className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-amber-500/10 border border-amber-500/15 text-[12px] text-amber-300 font-mono max-w-[200px]">
156
179
  <span className="truncate">{msg}</span>
@@ -195,92 +218,19 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
195
218
 
196
219
  <div className="flex items-center gap-1 px-4 pb-3.5">
197
220
  <button
198
- onClick={() => fileInputRef.current?.click()}
221
+ type="button"
222
+ onClick={() => setExtrasOpen((open) => !open)}
199
223
  className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
200
224
  text-text-3 text-[13px] cursor-pointer hover:text-text-2 hover:bg-white/[0.05] transition-all duration-200"
201
225
  style={{ fontFamily: 'inherit' }}
202
226
  >
203
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
204
- <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
227
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
228
+ <path d="M12 5v14" />
229
+ <path d="M5 12h14" />
205
230
  </svg>
206
- <span className="hidden sm:inline">Attach</span>
231
+ <span className="hidden sm:inline">Add</span>
207
232
  </button>
208
233
 
209
- <button
210
- onClick={() => imageInputRef.current?.click()}
211
- className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
212
- text-text-3 text-[13px] cursor-pointer hover:text-text-2 hover:bg-white/[0.05] transition-all duration-200"
213
- style={{ fontFamily: 'inherit' }}
214
- >
215
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
216
- <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
217
- <circle cx="8.5" cy="8.5" r="1.5" />
218
- <polyline points="21 15 16 10 5 21" />
219
- </svg>
220
- <span className="hidden sm:inline">Image</span>
221
- </button>
222
-
223
- {/* Plugin Chat Actions */}
224
- {pluginChatActions.map((action) => (
225
- <Tooltip key={action.id}>
226
- <TooltipTrigger asChild>
227
- <button
228
- onClick={() => {
229
- if (action.action === 'message') onSend(action.value)
230
- else if (action.action === 'link') window.open(action.value, '_blank')
231
- }}
232
- className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-emerald-500/[0.05]
233
- text-emerald-400 text-[13px] cursor-pointer hover:text-emerald-300 hover:bg-emerald-500/[0.1] transition-all duration-200"
234
- style={{ fontFamily: 'inherit' }}
235
- >
236
- {action.label}
237
- </button>
238
- </TooltipTrigger>
239
- {action.tooltip && <TooltipContent>{action.tooltip}</TooltipContent>}
240
- </Tooltip>
241
- ))}
242
-
243
- {micSupported && (
244
- <button
245
- onClick={toggleRecording}
246
- className={`flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
247
- text-[13px] cursor-pointer transition-all duration-200
248
- ${recording ? 'text-danger' : 'text-text-3 hover:text-text-2 hover:bg-white/[0.05]'}`}
249
- style={recording ? { animation: 'mic-pulse 1.5s ease-out infinite', fontFamily: 'inherit' } : { fontFamily: 'inherit' }}
250
- >
251
- <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
252
- <rect x="9" y="2" width="6" height="11" rx="3" />
253
- <path d="M5 10a7 7 0 0 0 14 0" />
254
- <line x1="12" y1="19" x2="12" y2="22" />
255
- </svg>
256
- </button>
257
- )}
258
-
259
- <Tooltip>
260
- <TooltipTrigger asChild>
261
- <button
262
- type="button"
263
- onClick={() => { useChatStore.getState().clearContext() }}
264
- disabled={streaming}
265
- className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
266
- text-text-3 text-[13px] cursor-pointer hover:text-amber-400 hover:bg-amber-400/10 transition-all duration-200 disabled:opacity-30 disabled:pointer-events-none"
267
- style={{ fontFamily: 'inherit' }}
268
- >
269
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
270
- <line x1="2" y1="12" x2="22" y2="12" />
271
- <polyline points="8 8 4 12 8 16" />
272
- <polyline points="16 8 20 12 16 16" />
273
- </svg>
274
- <span className="hidden sm:inline">New context</span>
275
- </button>
276
- </TooltipTrigger>
277
- <TooltipContent side="top" sideOffset={8}
278
- className="bg-raised border border-white/[0.08] text-text shadow-[0_8px_32px_rgba(0,0,0,0.5)] rounded-[10px] px-3.5 py-2.5 max-w-[220px]">
279
- <div className="font-display text-[12px] font-600 mb-0.5">New context window</div>
280
- <div className="text-[11px] text-text-3 leading-[1.4]">Adds a marker — messages above it won&apos;t be sent to the AI. Nothing is deleted.</div>
281
- </TooltipContent>
282
- </Tooltip>
283
-
284
234
  <div className="flex-1" />
285
235
 
286
236
  <span className="text-[11px] text-text-3/60 tabular-nums mr-2 font-mono">
@@ -314,6 +264,105 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
314
264
  </div>
315
265
  </div>
316
266
 
267
+ {extrasOpen && (
268
+ <div className="absolute left-0 bottom-[72px] w-[280px] max-w-[calc(100vw-2rem)] rounded-[16px] border border-white/[0.08] bg-raised/95 p-2 shadow-[0_18px_64px_rgba(0,0,0,0.55)] backdrop-blur-xl">
269
+ <button
270
+ type="button"
271
+ onClick={() => {
272
+ setExtrasOpen(false)
273
+ fileInputRef.current?.click()
274
+ }}
275
+ className="flex w-full items-center gap-2 rounded-[10px] px-3 py-2 text-left text-[13px] text-text-2 hover:bg-white/[0.05] cursor-pointer transition-colors"
276
+ style={{ fontFamily: 'inherit' }}
277
+ >
278
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
279
+ <path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
280
+ </svg>
281
+ Attach files
282
+ </button>
283
+ <button
284
+ type="button"
285
+ onClick={() => {
286
+ setExtrasOpen(false)
287
+ imageInputRef.current?.click()
288
+ }}
289
+ className="flex w-full items-center gap-2 rounded-[10px] px-3 py-2 text-left text-[13px] text-text-2 hover:bg-white/[0.05] cursor-pointer transition-colors"
290
+ style={{ fontFamily: 'inherit' }}
291
+ >
292
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
293
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
294
+ <circle cx="8.5" cy="8.5" r="1.5" />
295
+ <polyline points="21 15 16 10 5 21" />
296
+ </svg>
297
+ Add image
298
+ </button>
299
+ {micSupported && (
300
+ <button
301
+ type="button"
302
+ onClick={() => {
303
+ setExtrasOpen(false)
304
+ toggleRecording()
305
+ }}
306
+ className={`flex w-full items-center gap-2 rounded-[10px] px-3 py-2 text-left text-[13px] cursor-pointer transition-colors ${
307
+ recording ? 'text-danger bg-danger/[0.06]' : 'text-text-2 hover:bg-white/[0.05]'
308
+ }`}
309
+ style={recording ? { animation: 'mic-pulse 1.5s ease-out infinite', fontFamily: 'inherit' } : { fontFamily: 'inherit' }}
310
+ >
311
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
312
+ <rect x="9" y="2" width="6" height="11" rx="3" />
313
+ <path d="M5 10a7 7 0 0 0 14 0" />
314
+ <line x1="12" y1="19" x2="12" y2="22" />
315
+ </svg>
316
+ {recording ? 'Stop microphone' : 'Use microphone'}
317
+ </button>
318
+ )}
319
+ <button
320
+ type="button"
321
+ onClick={() => {
322
+ setExtrasOpen(false)
323
+ void useChatStore.getState().clearContext()
324
+ }}
325
+ disabled={streaming}
326
+ className="flex w-full items-center gap-2 rounded-[10px] px-3 py-2 text-left text-[13px] text-text-2 hover:bg-white/[0.05] cursor-pointer transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
327
+ style={{ fontFamily: 'inherit' }}
328
+ >
329
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
330
+ <line x1="2" y1="12" x2="22" y2="12" />
331
+ <polyline points="8 8 4 12 8 16" />
332
+ <polyline points="16 8 20 12 16 16" />
333
+ </svg>
334
+ New context window
335
+ </button>
336
+ {pluginChatActions.length > 0 && (
337
+ <>
338
+ <div className="mx-2 my-1 h-px bg-white/[0.06]" />
339
+ <div className="px-3 pb-1 pt-1 text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/50">
340
+ Quick actions
341
+ </div>
342
+ {pluginChatActions.map((action) => (
343
+ <Tooltip key={action.id}>
344
+ <TooltipTrigger asChild>
345
+ <button
346
+ type="button"
347
+ onClick={() => {
348
+ setExtrasOpen(false)
349
+ if (action.action === 'message') onSend(action.value)
350
+ else if (action.action === 'link') window.open(action.value, '_blank')
351
+ }}
352
+ className="flex w-full items-center gap-2 rounded-[10px] px-3 py-2 text-left text-[13px] text-emerald-300 hover:bg-emerald-500/[0.08] cursor-pointer transition-colors"
353
+ style={{ fontFamily: 'inherit' }}
354
+ >
355
+ {action.label}
356
+ </button>
357
+ </TooltipTrigger>
358
+ {action.tooltip && <TooltipContent>{action.tooltip}</TooltipContent>}
359
+ </Tooltip>
360
+ ))}
361
+ </>
362
+ )}
363
+ </div>
364
+ )}
365
+
317
366
  <input
318
367
  ref={fileInputRef}
319
368
  type="file"