@swarmclawai/swarmclaw 0.7.8 → 0.8.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 (251) hide show
  1. package/README.md +12 -15
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -3,13 +3,14 @@
3
3
  import { useEffect, useState, useCallback, useMemo } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
+ import { getPluginSourceLabel } from '@/lib/plugin-sources'
6
7
  import { toast } from 'sonner'
7
8
  import type { Agent, MarketplacePlugin, PluginMeta } from '@/types'
8
9
  import { AgentAvatar } from '@/components/agents/agent-avatar'
9
10
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
10
11
 
11
12
  type InstalledTab = 'core' | 'extensions'
12
- type TopTab = InstalledTab | 'swarmforge'
13
+ type TopTab = InstalledTab | 'marketplace'
13
14
 
14
15
  export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
15
16
  const plugins = useAppStore((s) => s.plugins)
@@ -56,7 +57,7 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
56
57
  }, [])
57
58
 
58
59
  useEffect(() => {
59
- if (inSidebar || tab !== 'swarmforge') return
60
+ if (inSidebar || tab !== 'marketplace') return
60
61
  const timer = setTimeout(() => { void loadMarketplace() }, 0)
61
62
  return () => clearTimeout(timer)
62
63
  }, [tab, inSidebar, loadMarketplace])
@@ -120,7 +121,13 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
120
121
  const toastId = toast.loading(`Installing ${p.name}...`)
121
122
  try {
122
123
  const safeFilename = `${p.id.replace(/[^a-zA-Z0-9.-]/g, '_')}.js`
123
- await api('POST', '/plugins/install', { url: p.url, filename: safeFilename })
124
+ await api('POST', '/plugins/install', {
125
+ url: p.url,
126
+ filename: safeFilename,
127
+ installMethod: 'marketplace',
128
+ sourceLabel: p.source,
129
+ installSource: p.catalogSource || p.source,
130
+ })
124
131
  await loadPlugins()
125
132
  toast.success(`Installed ${p.name}`, { id: toastId })
126
133
  } catch (err: unknown) {
@@ -181,8 +188,8 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
181
188
  <TabButton active={tab === 'extensions'} onClick={() => setTab('extensions')} count={extensionPlugins.length}>
182
189
  Extensions
183
190
  </TabButton>
184
- <TabButton active={tab === 'swarmforge'} onClick={() => setTab('swarmforge')}>
185
- SwarmForge
191
+ <TabButton active={tab === 'marketplace'} onClick={() => setTab('marketplace')}>
192
+ Marketplace
186
193
  </TabButton>
187
194
  </div>
188
195
 
@@ -214,17 +221,17 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
214
221
  emptyMessage={search ? 'No extensions match your search' : 'No extensions installed'}
215
222
  emptyAction={!search ? (
216
223
  <button
217
- onClick={() => setTab('swarmforge')}
224
+ onClick={() => setTab('marketplace')}
218
225
  className="mt-3 px-4 py-2 rounded-[10px] bg-transparent text-accent-bright text-[12px] font-600 cursor-pointer border border-accent-bright/20 hover:bg-accent-soft transition-all"
219
226
  style={{ fontFamily: 'inherit' }}
220
227
  >
221
- Browse SwarmForge
228
+ Browse Marketplace
222
229
  </button>
223
230
  ) : undefined}
224
231
  />
225
232
  )}
226
233
 
227
- {tab === 'swarmforge' && (
234
+ {tab === 'marketplace' && (
228
235
  <MarketplaceTab
229
236
  marketplace={marketplace}
230
237
  loading={mpLoading}
@@ -447,6 +454,12 @@ function PluginCard({ plugin, allowDelete, agents, onEdit, onToggle, onDelete, o
447
454
  {badge}
448
455
  </span>
449
456
  ))}
457
+ {plugin.sourceLabel && (
458
+ <SourceChip label={getPluginSourceLabel(plugin.sourceLabel)} tone="publisher" />
459
+ )}
460
+ {plugin.installSource && plugin.installSource !== plugin.sourceLabel && (
461
+ <SourceChip label={`via ${getPluginSourceLabel(plugin.installSource)}`} tone="catalog" />
462
+ )}
450
463
  {plugin.hasDependencyManifest && (
451
464
  <span className={`text-[10px] font-700 px-1.5 py-0.5 rounded-full ${
452
465
  plugin.dependencyInstallStatus === 'installed'
@@ -550,7 +563,14 @@ function MarketplaceTab({ marketplace, loading, installing, installedFilenames,
550
563
  const q = search.toLowerCase()
551
564
  const filtered = marketplace
552
565
  .filter((p) => {
553
- if (q && !p.name.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q) && !(p.tags ?? []).some((t) => t.toLowerCase().includes(q))) return false
566
+ const sourceTerms = [getPluginSourceLabel(p.source).toLowerCase(), getPluginSourceLabel(p.catalogSource).toLowerCase()]
567
+ if (
568
+ q
569
+ && !p.name.toLowerCase().includes(q)
570
+ && !p.description.toLowerCase().includes(q)
571
+ && !(p.tags ?? []).some((t) => t.toLowerCase().includes(q))
572
+ && !sourceTerms.some((term) => term.includes(q))
573
+ ) return false
554
574
  if (activeTag && !(p.tags ?? []).includes(activeTag)) return false
555
575
  return true
556
576
  })
@@ -606,6 +626,12 @@ function MarketplaceTab({ marketplace, loading, installing, installedFilenames,
606
626
  <span className="text-[10px] font-mono text-text-3/70">v{p.version}</span>
607
627
  {p.openclaw && <span className="text-[9px] font-600 text-emerald-400 bg-emerald-400/10 px-1.5 py-0.5 rounded-full">OpenClaw</span>}
608
628
  </div>
629
+ <div className="flex items-center gap-1.5 mt-2 flex-wrap">
630
+ {p.source && <SourceChip label={getPluginSourceLabel(p.source)} tone="publisher" />}
631
+ {p.catalogSource && p.catalogSource !== p.source && (
632
+ <SourceChip label={`via ${getPluginSourceLabel(p.catalogSource)}`} tone="catalog" />
633
+ )}
634
+ </div>
609
635
  <div className="text-[11px] text-text-3/60 mt-1 line-clamp-2">{p.description}</div>
610
636
  <div className="flex items-center gap-2 mt-2">
611
637
  <span className="text-[10px] text-text-3/70">by {p.author}</span>
@@ -645,3 +671,13 @@ function MarketplaceTab({ marketplace, loading, installing, installedFilenames,
645
671
  </div>
646
672
  )
647
673
  }
674
+
675
+ function SourceChip({ label, tone }: { label: string; tone: 'publisher' | 'catalog' }) {
676
+ return (
677
+ <span className={tone === 'publisher'
678
+ ? 'text-[10px] font-700 px-1.5 py-0.5 rounded-full bg-sky-500/10 text-sky-300'
679
+ : 'text-[10px] font-700 px-1.5 py-0.5 rounded-full bg-white/[0.05] text-text-3/75'}>
680
+ {label}
681
+ </span>
682
+ )
683
+ }
@@ -5,6 +5,7 @@ import { useAppStore } from '@/stores/use-app-store'
5
5
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
6
  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
7
7
  import { api } from '@/lib/api-client'
8
+ import { getPluginSourceLabel } from '@/lib/plugin-sources'
8
9
  import { toast } from 'sonner'
9
10
  import type { PluginMeta, PluginSettingsField, MarketplacePlugin } from '@/types'
10
11
 
@@ -176,7 +177,13 @@ export function PluginSheet() {
176
177
  const toastId = toast.loading(`Installing ${p.name}...`)
177
178
  try {
178
179
  const safeFilename = `${p.id.replace(/[^a-zA-Z0-9.-]/g, '_')}.js`
179
- await api('POST', '/plugins/install', { url: p.url, filename: safeFilename })
180
+ await api('POST', '/plugins/install', {
181
+ url: p.url,
182
+ filename: safeFilename,
183
+ installMethod: 'marketplace',
184
+ sourceLabel: p.source,
185
+ installSource: p.catalogSource || p.source,
186
+ })
180
187
  await loadPlugins()
181
188
  toast.success(`Installed ${p.name}`, { id: toastId })
182
189
  } catch (err: unknown) {
@@ -231,19 +238,31 @@ export function PluginSheet() {
231
238
  </span>
232
239
  </div>
233
240
 
234
- <div className="grid grid-cols-2 gap-2 mt-3">
241
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mt-3">
235
242
  <div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
236
- <div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Source</div>
243
+ <div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Type</div>
237
244
  <div className="text-[11px] text-text-2">
238
- {editing.isBuiltin ? 'Core Platform' : editing.source === 'marketplace' ? 'Marketplace Extension' : 'Local Extension'}
245
+ {editing.isBuiltin
246
+ ? 'Core Platform'
247
+ : editing.source === 'marketplace'
248
+ ? 'Marketplace Extension'
249
+ : editing.source === 'manual'
250
+ ? 'Manual URL Extension'
251
+ : 'Local Extension'}
239
252
  </div>
240
253
  </div>
241
254
  <div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
242
- <div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Author</div>
243
- <div className="text-[11px] text-text-2">{editing.author || 'Unknown'}</div>
255
+ <div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Publisher</div>
256
+ <div className="text-[11px] text-text-2">{getPluginSourceLabel(editing.sourceLabel)}</div>
257
+ </div>
258
+ <div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
259
+ <div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Installed Via</div>
260
+ <div className="text-[11px] text-text-2">{getPluginSourceLabel(editing.installSource || editing.sourceLabel)}</div>
244
261
  </div>
245
262
  </div>
246
263
 
264
+ <div className="text-[11px] text-text-3/60 mt-2">{editing.author || 'Unknown author'}</div>
265
+
247
266
  <div className="text-[11px] font-mono text-text-3/60 mt-3 break-all">{editing.filename}</div>
248
267
  <div className="flex items-center gap-1.5 mt-2 flex-wrap">
249
268
  {pluginCapabilityBadges(editing).length > 0 ? (
@@ -255,6 +274,16 @@ export function PluginSheet() {
255
274
  ) : (
256
275
  <span className="text-[10px] text-text-3/50">No declared tools/hooks metadata</span>
257
276
  )}
277
+ {editing.sourceLabel && (
278
+ <span className="text-[10px] font-700 px-1.5 py-0.5 rounded-full bg-sky-500/10 text-sky-300">
279
+ {getPluginSourceLabel(editing.sourceLabel)}
280
+ </span>
281
+ )}
282
+ {editing.installSource && editing.installSource !== editing.sourceLabel && (
283
+ <span className="text-[10px] font-700 px-1.5 py-0.5 rounded-full bg-white/[0.05] text-text-3/75">
284
+ via {getPluginSourceLabel(editing.installSource)}
285
+ </span>
286
+ )}
258
287
  </div>
259
288
 
260
289
  {editing.autoDisabled && (
@@ -392,7 +421,14 @@ export function PluginSheet() {
392
421
  const q = search.toLowerCase()
393
422
  const filtered = marketplace
394
423
  .filter((p) => {
395
- if (q && !p.name.toLowerCase().includes(q) && !p.description.toLowerCase().includes(q) && !(p.tags ?? []).some((t) => t.toLowerCase().includes(q))) return false
424
+ const sourceTerms = [getPluginSourceLabel(p.source).toLowerCase(), getPluginSourceLabel(p.catalogSource).toLowerCase()]
425
+ if (
426
+ q
427
+ && !p.name.toLowerCase().includes(q)
428
+ && !p.description.toLowerCase().includes(q)
429
+ && !(p.tags ?? []).some((t) => t.toLowerCase().includes(q))
430
+ && !sourceTerms.some((term) => term.includes(q))
431
+ ) return false
396
432
  if (activeTag && !(p.tags ?? []).includes(activeTag)) return false
397
433
  return true
398
434
  })
@@ -458,6 +494,18 @@ export function PluginSheet() {
458
494
  <span className="text-[10px] font-mono text-text-3/70">v{p.version}</span>
459
495
  {p.openclaw && <span className="text-[9px] font-600 text-emerald-400 bg-emerald-400/10 px-1.5 py-0.5 rounded-full">OpenClaw</span>}
460
496
  </div>
497
+ <div className="flex items-center gap-1.5 mt-2 flex-wrap">
498
+ {p.source && (
499
+ <span className="text-[10px] font-700 px-1.5 py-0.5 rounded-full bg-sky-500/10 text-sky-300">
500
+ {getPluginSourceLabel(p.source)}
501
+ </span>
502
+ )}
503
+ {p.catalogSource && p.catalogSource !== p.source && (
504
+ <span className="text-[10px] font-700 px-1.5 py-0.5 rounded-full bg-white/[0.05] text-text-3/75">
505
+ via {getPluginSourceLabel(p.catalogSource)}
506
+ </span>
507
+ )}
508
+ </div>
461
509
  <div className="text-[11px] text-text-3/60 mt-1">{p.description}</div>
462
510
  <div className="flex items-center gap-2 mt-2">
463
511
  <span className="text-[10px] text-text-3/70">by {p.author}</span>
@@ -125,6 +125,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
125
125
  ok: boolean
126
126
  verify?: {
127
127
  ok: boolean
128
+ message?: string
128
129
  error?: string
129
130
  hint?: string
130
131
  models?: string[]
@@ -150,7 +151,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
150
151
  lastVerifiedOk: verify.verify ? verifiedOk : (existing?.deployment?.lastVerifiedOk ?? null),
151
152
  lastVerifiedMessage: verify.verify
152
153
  ? (verifiedOk
153
- ? `Verified during save with ${verify.verify.models?.length || 0} model${(verify.verify.models?.length || 0) === 1 ? '' : 's'}.`
154
+ ? (verify.verify.message || `Verified during save with ${verify.verify.models?.length || 0} model${(verify.verify.models?.length || 0) === 1 ? '' : 's'}.`)
154
155
  : (verify.verify.error || verify.verify.hint || 'Verification failed.'))
155
156
  : (existing?.deployment?.lastVerifiedMessage || null),
156
157
  },
@@ -6,6 +6,7 @@ import { createProviderConfig, updateProviderConfig, deleteProviderConfig } from
6
6
  import { api } from '@/lib/api-client'
7
7
  import { fetchProviderModelDiscovery } from '@/lib/provider-model-discovery-client'
8
8
  import { BottomSheet } from '@/components/shared/bottom-sheet'
9
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
9
10
  import { toast } from 'sonner'
10
11
 
11
12
  export function ProviderSheet() {
@@ -40,6 +41,8 @@ export function ProviderSheet() {
40
41
  const [liveLoading, setLiveLoading] = useState(false)
41
42
  const [liveMessage, setLiveMessage] = useState('')
42
43
  const [liveCached, setLiveCached] = useState(false)
44
+ const [confirmDelete, setConfirmDelete] = useState(false)
45
+ const [deleting, setDeleting] = useState(false)
43
46
 
44
47
  // Find editing provider in custom configs OR built-in list
45
48
  const editingCustom = editingId ? providerConfigs.find((c) => c.id === editingId) : null
@@ -122,6 +125,8 @@ export function ProviderSheet() {
122
125
  }
123
126
 
124
127
  const onClose = () => {
128
+ setConfirmDelete(false)
129
+ setDeleting(false)
125
130
  setOpen(false)
126
131
  setEditingId(null)
127
132
  }
@@ -162,14 +167,17 @@ export function ProviderSheet() {
162
167
 
163
168
  const handleDelete = async () => {
164
169
  if (editingCustom) {
165
- if (!confirm(`Delete custom provider "${editingCustom.name}"?`)) return
170
+ setDeleting(true)
166
171
  try {
167
172
  await deleteProviderConfig(editingCustom.id)
168
173
  toast.success('Provider deleted')
169
174
  await loadProviderConfigs()
175
+ setConfirmDelete(false)
170
176
  onClose()
171
177
  } catch (err: unknown) {
172
178
  toast.error(err instanceof Error ? err.message : 'Failed to delete provider')
179
+ } finally {
180
+ setDeleting(false)
173
181
  }
174
182
  }
175
183
  }
@@ -493,7 +501,7 @@ export function ProviderSheet() {
493
501
 
494
502
  <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
495
503
  {editingCustom && (
496
- <button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
504
+ <button onClick={() => setConfirmDelete(true)} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
497
505
  Delete
498
506
  </button>
499
507
  )}
@@ -520,6 +528,17 @@ export function ProviderSheet() {
520
528
  </button>
521
529
  )}
522
530
  </div>
531
+ <ConfirmDialog
532
+ open={confirmDelete}
533
+ title="Delete Provider?"
534
+ message={editingCustom ? `Delete custom provider "${editingCustom.name}"?` : 'Delete this provider?'}
535
+ confirmLabel={deleting ? 'Deleting...' : 'Delete'}
536
+ confirmDisabled={deleting}
537
+ cancelDisabled={deleting}
538
+ danger
539
+ onConfirm={() => { void handleDelete() }}
540
+ onCancel={() => { if (!deleting) setConfirmDelete(false) }}
541
+ />
523
542
  </BottomSheet>
524
543
  )
525
544
  }
@@ -4,6 +4,8 @@ import type { Schedule } from '@/types'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
6
  import { cronToHuman } from '@/lib/cron-human'
7
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
8
+ import { isUserCreatedSchedule } from '@/lib/schedule-origin'
7
9
 
8
10
  const STATUS_COLORS: Record<string, string> = {
9
11
  active: 'text-emerald-400 bg-emerald-400/[0.08]',
@@ -55,6 +57,7 @@ export function ScheduleCard({ schedule, inSidebar, index = 0 }: Props) {
55
57
  }
56
58
 
57
59
  const agent = agents[schedule.agentId]
60
+ const creatorAgent = schedule.createdByAgentId ? agents[schedule.createdByAgentId] : null
58
61
  const statusClass = STATUS_COLORS[schedule.status] || STATUS_COLORS.paused
59
62
  const canToggle = schedule.status === 'active' || schedule.status === 'paused'
60
63
 
@@ -101,7 +104,7 @@ export function ScheduleCard({ schedule, inSidebar, index = 0 }: Props) {
101
104
  </div>
102
105
  </div>
103
106
  <div className="text-[12px] text-text-3/70 mt-1.5 truncate">
104
- {agent?.name || 'Unknown agent'} &middot; {schedule.scheduleType}
107
+ Runs on {agent?.name || 'Unknown agent'} &middot; {schedule.scheduleType}
105
108
  {!inSidebar && schedule.scheduleType === 'cron' && schedule.cron && (
106
109
  <span className="text-text-3/50 ml-1" title={schedule.cron}>({cronToHuman(schedule.cron)})</span>
107
110
  )}
@@ -113,6 +116,27 @@ export function ScheduleCard({ schedule, inSidebar, index = 0 }: Props) {
113
116
  </span>
114
117
  )}
115
118
  </div>
119
+ <div className="flex flex-wrap items-center gap-1.5 mt-2">
120
+ {creatorAgent ? (
121
+ <span className="inline-flex max-w-full items-center gap-1.5 rounded-[7px] bg-white/[0.05] px-2 py-1 text-[10px] font-600 text-text-2">
122
+ <AgentAvatar
123
+ seed={creatorAgent.avatarSeed}
124
+ avatarUrl={creatorAgent.avatarUrl}
125
+ name={creatorAgent.name}
126
+ size={14}
127
+ />
128
+ <span className="truncate">Created by {creatorAgent.name}</span>
129
+ </span>
130
+ ) : (
131
+ <span className="inline-flex max-w-full items-center gap-1.5 rounded-[7px] bg-white/[0.04] px-2 py-1 text-[10px] font-600 text-text-3">
132
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
133
+ <circle cx="12" cy="8" r="4" />
134
+ <path d="M4 20c1.5-3.5 4.6-5 8-5s6.5 1.5 8 5" />
135
+ </svg>
136
+ <span className="truncate">{isUserCreatedSchedule(schedule) ? 'Created manually' : 'Creator unknown'}</span>
137
+ </span>
138
+ )}
139
+ </div>
116
140
  <div className="text-[11px] text-text-3/60 mt-1">
117
141
  Next: {formatNext(schedule.nextRunAt)}
118
142
  </div>
@@ -5,12 +5,15 @@ import { useAppStore } from '@/stores/use-app-store'
5
5
  import { createSchedule, updateSchedule, deleteSchedule } from '@/lib/schedules'
6
6
  import { BottomSheet } from '@/components/shared/bottom-sheet'
7
7
  import { AgentPickerList } from '@/components/shared/agent-picker-list'
8
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
8
9
  import { inputClass } from '@/components/shared/form-styles'
10
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
9
11
  import type { ScheduleType, ScheduleStatus } from '@/types'
10
12
  import cronstrue from 'cronstrue'
11
13
  import { SectionLabel } from '@/components/shared/section-label'
12
14
  import { SCHEDULE_TEMPLATES, type ScheduleTemplate } from '@/lib/schedule-templates'
13
15
  import { HintTip } from '@/components/shared/hint-tip'
16
+ import { isUserCreatedSchedule } from '@/lib/schedule-origin'
14
17
  import { toast } from 'sonner'
15
18
  import {
16
19
  Newspaper, BarChart3, HeartPulse, PenLine, Trash2,
@@ -102,6 +105,8 @@ export function ScheduleSheet() {
102
105
  const [intervalMs, setIntervalMs] = useState(3600000)
103
106
  const [status, setStatus] = useState<ScheduleStatus>('active')
104
107
  const [customCron, setCustomCron] = useState(false)
108
+ const [confirmDelete, setConfirmDelete] = useState(false)
109
+ const [deleting, setDeleting] = useState(false)
105
110
 
106
111
  const editing = editingId ? schedules[editingId] : null
107
112
  const isCreating = !editing
@@ -163,6 +168,8 @@ export function ScheduleSheet() {
163
168
  }, [cron])
164
169
 
165
170
  const onClose = () => {
171
+ setConfirmDelete(false)
172
+ setDeleting(false)
166
173
  setOpen(false)
167
174
  setEditingId(null)
168
175
  }
@@ -195,14 +202,17 @@ export function ScheduleSheet() {
195
202
 
196
203
  const handleDelete = async () => {
197
204
  if (!editing) return
198
- if (!confirm(`Delete schedule "${editing.name}"?`)) return
205
+ setDeleting(true)
199
206
  try {
200
207
  await deleteSchedule(editing.id)
201
208
  toast.success('Schedule deleted')
202
209
  await loadSchedules()
210
+ setConfirmDelete(false)
203
211
  onClose()
204
212
  } catch (err: unknown) {
205
213
  toast.error(err instanceof Error ? err.message : 'Failed to delete schedule')
214
+ } finally {
215
+ setDeleting(false)
206
216
  }
207
217
  }
208
218
 
@@ -211,6 +221,7 @@ export function ScheduleSheet() {
211
221
  const step1Valid = scheduleType === 'cron' ? cron.trim().length > 0 : intervalMs > 0
212
222
 
213
223
  const selectedAgent = agentId ? agents[agentId] : null
224
+ const creatorAgent = editing?.createdByAgentId ? agents[editing.createdByAgentId] : null
214
225
 
215
226
  return (
216
227
  <BottomSheet open={open} onClose={onClose} wide>
@@ -466,6 +477,26 @@ export function ScheduleSheet() {
466
477
  <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Agent</span>
467
478
  <div className="text-[14px] text-text font-600 mt-0.5">{selectedAgent?.name || agentId}</div>
468
479
  </div>
480
+ {editing && (
481
+ <div>
482
+ <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Created By</span>
483
+ {creatorAgent ? (
484
+ <div className="mt-1 inline-flex items-center gap-2 rounded-[10px] bg-white/[0.04] px-3 py-2 text-[13px] text-text-2">
485
+ <AgentAvatar
486
+ seed={creatorAgent.avatarSeed}
487
+ avatarUrl={creatorAgent.avatarUrl}
488
+ name={creatorAgent.name}
489
+ size={18}
490
+ />
491
+ <span>{creatorAgent.name}</span>
492
+ </div>
493
+ ) : (
494
+ <div className="text-[13px] text-text-2 mt-0.5">
495
+ {isUserCreatedSchedule(editing) ? 'Manual / user-created' : 'Unknown'}
496
+ </div>
497
+ )}
498
+ </div>
499
+ )}
469
500
  <div>
470
501
  <span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Task</span>
471
502
  <div className="text-[13px] text-text-2 mt-0.5 whitespace-pre-wrap">{taskPrompt}</div>
@@ -497,7 +528,7 @@ export function ScheduleSheet() {
497
528
  {/* Footer */}
498
529
  <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
499
530
  {editing && step === 0 && (
500
- <button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
531
+ <button onClick={() => setConfirmDelete(true)} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
501
532
  Delete
502
533
  </button>
503
534
  )}
@@ -547,6 +578,17 @@ export function ScheduleSheet() {
547
578
  </button>
548
579
  )}
549
580
  </div>
581
+ <ConfirmDialog
582
+ open={confirmDelete}
583
+ title="Delete Schedule?"
584
+ message={editing ? `Delete "${editing.name}"? This will remove the schedule from the app.` : 'Delete this schedule?'}
585
+ confirmLabel={deleting ? 'Deleting...' : 'Delete'}
586
+ confirmDisabled={deleting}
587
+ cancelDisabled={deleting}
588
+ danger
589
+ onConfirm={() => { void handleDelete() }}
590
+ onCancel={() => { if (!deleting) setConfirmDelete(false) }}
591
+ />
550
592
  </BottomSheet>
551
593
  )
552
594
  }
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
+ import { ConfirmDialog } from '@/components/shared/confirm-dialog'
6
7
  import { AgentAvatar } from '@/components/agents/agent-avatar'
7
8
  import { api } from '@/lib/api-client'
8
9
  import { toast } from 'sonner'
@@ -25,6 +26,8 @@ export function SecretSheet() {
25
26
  const [scope, setScope] = useState<'global' | 'agent'>('global')
26
27
  const [agentIds, setAgentIds] = useState<string[]>([])
27
28
  const [saving, setSaving] = useState(false)
29
+ const [confirmDelete, setConfirmDelete] = useState(false)
30
+ const [deleting, setDeleting] = useState(false)
28
31
 
29
32
  const editing = editingId ? secrets[editingId] : null
30
33
  const agentList = Object.values(agents)
@@ -50,6 +53,8 @@ export function SecretSheet() {
50
53
  }, [editing, open])
51
54
 
52
55
  const handleClose = () => {
56
+ setConfirmDelete(false)
57
+ setDeleting(false)
53
58
  setOpen(false)
54
59
  setEditingId(null)
55
60
  }
@@ -87,14 +92,17 @@ export function SecretSheet() {
87
92
 
88
93
  const handleDelete = async () => {
89
94
  if (!editing) return
90
- if (!confirm(`Delete secret "${editing.name}"?`)) return
95
+ setDeleting(true)
91
96
  try {
92
97
  await api('DELETE', `/secrets/${editing.id}`)
93
98
  toast.success('Secret deleted')
94
99
  await loadSecrets()
100
+ setConfirmDelete(false)
95
101
  handleClose()
96
102
  } catch (err: unknown) {
97
103
  toast.error(err instanceof Error ? err.message : 'Failed to delete secret')
104
+ } finally {
105
+ setDeleting(false)
98
106
  }
99
107
  }
100
108
 
@@ -184,7 +192,7 @@ export function SecretSheet() {
184
192
  <div className="flex gap-3 pt-3">
185
193
  {editing && (
186
194
  <button
187
- onClick={handleDelete}
195
+ onClick={() => setConfirmDelete(true)}
188
196
  className="px-5 py-3 rounded-[14px] border border-danger/30 bg-transparent text-danger text-[14px] font-600 cursor-pointer hover:bg-danger/10 transition-colors"
189
197
  style={{ fontFamily: 'inherit' }}
190
198
  >
@@ -202,6 +210,17 @@ export function SecretSheet() {
202
210
  {saving ? 'Saving...' : editing ? 'Update' : 'Save'}
203
211
  </button>
204
212
  </div>
213
+ <ConfirmDialog
214
+ open={confirmDelete}
215
+ title="Delete Secret?"
216
+ message={editing ? `Delete "${editing.name}"? This will remove the stored secret from the app.` : 'Delete this secret?'}
217
+ confirmLabel={deleting ? 'Deleting...' : 'Delete'}
218
+ confirmDisabled={deleting}
219
+ cancelDisabled={deleting}
220
+ danger
221
+ onConfirm={() => { void handleDelete() }}
222
+ onCancel={() => { if (!deleting) setConfirmDelete(false) }}
223
+ />
205
224
  </div>
206
225
  </BottomSheet>
207
226
  )
@@ -4,6 +4,7 @@ import { useState, useEffect, useRef, useCallback, useMemo } from 'react'
4
4
  import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
5
5
  import { useAppStore } from '@/stores/use-app-store'
6
6
  import { AgentAvatar } from '@/components/agents/agent-avatar'
7
+ import { toast } from 'sonner'
7
8
 
8
9
  export function AgentSwitchDialog() {
9
10
  const [open, setOpen] = useState(false)
@@ -47,10 +48,15 @@ export function AgentSwitchDialog() {
47
48
  }, [agents, query])
48
49
 
49
50
  const handleSelect = useCallback((agentId: string) => {
51
+ const agent = agents[agentId]
52
+ if (agent?.disabled === true && !agent.threadSessionId) {
53
+ toast.error(`${agent.name} is disabled. Re-enable it to start a new chat.`)
54
+ return
55
+ }
50
56
  setOpen(false)
51
57
  void setCurrentAgent(agentId)
52
58
  // eslint-disable-next-line react-hooks/exhaustive-deps
53
- }, [])
59
+ }, [agents, setCurrentAgent])
54
60
 
55
61
  const handleKeyDown = (e: React.KeyboardEvent) => {
56
62
  if (e.key === 'ArrowDown') {
@@ -119,6 +125,11 @@ export function AgentSwitchDialog() {
119
125
  <div className="flex-1 min-w-0">
120
126
  <div className="flex items-center gap-2">
121
127
  <span className="text-[13px] font-500 text-text truncate">{agent.name}</span>
128
+ {agent.disabled === true && (
129
+ <span className="px-1.5 py-0.5 rounded-[4px] bg-amber-400/[0.08] text-[10px] font-500 text-amber-300 shrink-0">
130
+ disabled
131
+ </span>
132
+ )}
122
133
  {agent.id === currentAgentId && (
123
134
  <span className="px-1.5 py-0.5 rounded-[4px] bg-accent-bright/15 text-[10px] font-500 text-accent-bright shrink-0">
124
135
  current
@@ -9,9 +9,11 @@ interface Props {
9
9
  onClose: () => void
10
10
  children: ReactNode
11
11
  wide?: boolean
12
+ title?: string
13
+ description?: string
12
14
  }
13
15
 
14
- export function BottomSheet({ open, onClose, children, wide }: Props) {
16
+ export function BottomSheet({ open, onClose, children, wide, title, description }: Props) {
15
17
  return (
16
18
  <DialogPrimitive.Root open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>
17
19
  <DialogPrimitive.Portal>
@@ -27,10 +29,18 @@ export function BottomSheet({ open, onClose, children, wide }: Props) {
27
29
  ${wide ? 'sm:max-w-[760px]' : 'sm:max-w-[560px]'}`}
28
30
  style={{ animationDuration: '220ms' }}
29
31
  >
30
- <div className="relative shrink-0 px-4 pt-3 sm:px-5 sm:pt-5">
32
+ <div className="relative shrink-0 px-4 pt-4 pr-14 sm:px-5 sm:pt-6 sm:pr-16">
31
33
  <div className="mx-auto h-1 w-10 rounded-full bg-white/[0.08] sm:hidden" />
34
+ <DialogPrimitive.Title className="sr-only">
35
+ {title || 'Dialog'}
36
+ </DialogPrimitive.Title>
37
+ {description ? (
38
+ <DialogPrimitive.Description className="sr-only">
39
+ {description}
40
+ </DialogPrimitive.Description>
41
+ ) : null}
32
42
  <DialogPrimitive.Close
33
- className="absolute right-3 top-2.5 inline-flex h-9 w-9 items-center justify-center rounded-[12px] border border-white/[0.06] bg-white/[0.03] text-text-3 transition-all hover:bg-white/[0.06] hover:text-text-2 focus:outline-none focus:ring-2 focus:ring-accent-bright/30 sm:right-4 sm:top-4"
43
+ className="absolute right-4 top-3.5 inline-flex h-9 w-9 items-center justify-center rounded-[12px] border border-white/[0.06] bg-white/[0.03] text-text-3 transition-all hover:bg-white/[0.06] hover:text-text-2 focus:outline-none focus:ring-2 focus:ring-accent-bright/30 sm:right-5 sm:top-5"
34
44
  >
35
45
  <XIcon className="size-4" />
36
46
  <span className="sr-only">Close</span>