@swarmclawai/swarmclaw 0.7.2 → 0.7.3

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 (197) hide show
  1. package/README.md +81 -22
  2. package/package.json +1 -1
  3. package/src/app/api/agents/[id]/route.ts +26 -0
  4. package/src/app/api/agents/[id]/thread/route.ts +36 -7
  5. package/src/app/api/agents/route.ts +12 -1
  6. package/src/app/api/auth/route.ts +76 -7
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +7 -2
  8. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  9. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  10. package/src/app/api/chats/[id]/main-loop/route.ts +7 -88
  11. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  12. package/src/app/api/chats/[id]/route.ts +18 -0
  13. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  14. package/src/app/api/chats/route.ts +16 -0
  15. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  16. package/src/app/api/connectors/doctor/route.ts +13 -0
  17. package/src/app/api/files/open/route.ts +16 -14
  18. package/src/app/api/memory/maintenance/route.ts +11 -1
  19. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  20. package/src/app/api/openclaw/skills/route.ts +11 -3
  21. package/src/app/api/plugins/dependencies/route.ts +24 -0
  22. package/src/app/api/plugins/install/route.ts +15 -92
  23. package/src/app/api/plugins/route.ts +3 -26
  24. package/src/app/api/plugins/settings/route.ts +17 -12
  25. package/src/app/api/plugins/ui/route.ts +1 -0
  26. package/src/app/api/settings/route.ts +49 -7
  27. package/src/app/api/tasks/[id]/route.ts +15 -6
  28. package/src/app/api/tasks/bulk/route.ts +2 -2
  29. package/src/app/api/tasks/route.ts +9 -4
  30. package/src/app/api/webhooks/[id]/route.ts +8 -1
  31. package/src/app/page.tsx +9 -2
  32. package/src/cli/index.js +4 -0
  33. package/src/cli/index.ts +3 -10
  34. package/src/components/agents/agent-card.tsx +15 -12
  35. package/src/components/agents/agent-chat-list.tsx +101 -1
  36. package/src/components/agents/agent-list.tsx +46 -9
  37. package/src/components/agents/agent-sheet.tsx +207 -16
  38. package/src/components/agents/inspector-panel.tsx +108 -48
  39. package/src/components/auth/access-key-gate.tsx +36 -97
  40. package/src/components/chat/chat-area.tsx +29 -13
  41. package/src/components/chat/chat-card.tsx +4 -20
  42. package/src/components/chat/chat-header.tsx +255 -353
  43. package/src/components/chat/chat-list.tsx +7 -9
  44. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  45. package/src/components/chat/message-list.tsx +3 -1
  46. package/src/components/chatrooms/chatroom-view.tsx +347 -205
  47. package/src/components/connectors/connector-list.tsx +265 -127
  48. package/src/components/connectors/connector-sheet.tsx +217 -0
  49. package/src/components/home/home-view.tsx +128 -4
  50. package/src/components/layout/app-layout.tsx +383 -194
  51. package/src/components/layout/mobile-header.tsx +26 -8
  52. package/src/components/plugins/plugin-list.tsx +15 -3
  53. package/src/components/plugins/plugin-sheet.tsx +118 -9
  54. package/src/components/projects/project-detail.tsx +183 -0
  55. package/src/components/shared/agent-picker-list.tsx +2 -2
  56. package/src/components/shared/command-palette.tsx +111 -24
  57. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  58. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  59. package/src/components/shared/settings/section-heartbeat.tsx +77 -0
  60. package/src/components/shared/settings/section-orchestrator.tsx +3 -3
  61. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  62. package/src/components/shared/settings/section-secrets.tsx +6 -6
  63. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  64. package/src/components/shared/settings/section-voice.tsx +5 -1
  65. package/src/components/shared/settings/section-web-search.tsx +10 -2
  66. package/src/components/shared/settings/settings-page.tsx +245 -46
  67. package/src/components/tasks/approvals-panel.tsx +205 -18
  68. package/src/components/tasks/task-board.tsx +242 -46
  69. package/src/components/usage/metrics-dashboard.tsx +74 -1
  70. package/src/components/wallets/wallet-panel.tsx +17 -5
  71. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  72. package/src/lib/auth.ts +17 -0
  73. package/src/lib/chat-streaming-state.test.ts +108 -0
  74. package/src/lib/chat-streaming-state.ts +108 -0
  75. package/src/lib/openclaw-agent-id.test.ts +14 -0
  76. package/src/lib/openclaw-agent-id.ts +31 -0
  77. package/src/lib/server/agent-assignment.test.ts +112 -0
  78. package/src/lib/server/agent-assignment.ts +169 -0
  79. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  80. package/src/lib/server/approvals-auto-approve.test.ts +205 -0
  81. package/src/lib/server/approvals.ts +483 -75
  82. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  83. package/src/lib/server/browser-state.test.ts +118 -0
  84. package/src/lib/server/browser-state.ts +123 -0
  85. package/src/lib/server/build-llm.test.ts +36 -0
  86. package/src/lib/server/build-llm.ts +11 -4
  87. package/src/lib/server/builtin-plugins.ts +34 -0
  88. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  89. package/src/lib/server/chat-execution-tool-events.test.ts +134 -0
  90. package/src/lib/server/chat-execution.ts +250 -61
  91. package/src/lib/server/chatroom-health.test.ts +26 -0
  92. package/src/lib/server/chatroom-health.ts +2 -3
  93. package/src/lib/server/chatroom-helpers.test.ts +67 -2
  94. package/src/lib/server/chatroom-helpers.ts +45 -5
  95. package/src/lib/server/connectors/discord.ts +175 -11
  96. package/src/lib/server/connectors/doctor.test.ts +80 -0
  97. package/src/lib/server/connectors/doctor.ts +116 -0
  98. package/src/lib/server/connectors/manager.ts +946 -110
  99. package/src/lib/server/connectors/policy.test.ts +222 -0
  100. package/src/lib/server/connectors/policy.ts +452 -0
  101. package/src/lib/server/connectors/slack.ts +188 -9
  102. package/src/lib/server/connectors/telegram.ts +65 -15
  103. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  104. package/src/lib/server/connectors/thread-context.ts +72 -0
  105. package/src/lib/server/connectors/types.ts +41 -11
  106. package/src/lib/server/daemon-state.ts +59 -1
  107. package/src/lib/server/data-dir.ts +13 -0
  108. package/src/lib/server/delegation-jobs.test.ts +140 -0
  109. package/src/lib/server/delegation-jobs.ts +248 -0
  110. package/src/lib/server/document-utils.test.ts +47 -0
  111. package/src/lib/server/document-utils.ts +397 -0
  112. package/src/lib/server/heartbeat-service.ts +13 -39
  113. package/src/lib/server/heartbeat-source.test.ts +22 -0
  114. package/src/lib/server/heartbeat-source.ts +7 -0
  115. package/src/lib/server/identity-continuity.test.ts +77 -0
  116. package/src/lib/server/identity-continuity.ts +127 -0
  117. package/src/lib/server/mailbox-utils.ts +347 -0
  118. package/src/lib/server/main-agent-loop.ts +27 -967
  119. package/src/lib/server/memory-db.ts +4 -6
  120. package/src/lib/server/memory-tiers.ts +40 -0
  121. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  122. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  123. package/src/lib/server/openclaw-exec-config.ts +5 -6
  124. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  125. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  126. package/src/lib/server/openclaw-sync.ts +3 -2
  127. package/src/lib/server/orchestrator-lg.ts +17 -6
  128. package/src/lib/server/orchestrator.ts +2 -2
  129. package/src/lib/server/playwright-proxy.mjs +27 -3
  130. package/src/lib/server/plugins.test.ts +207 -0
  131. package/src/lib/server/plugins.ts +822 -69
  132. package/src/lib/server/provider-health.ts +33 -3
  133. package/src/lib/server/queue.ts +3 -20
  134. package/src/lib/server/scheduler.ts +2 -0
  135. package/src/lib/server/session-archive-memory.test.ts +85 -0
  136. package/src/lib/server/session-archive-memory.ts +230 -0
  137. package/src/lib/server/session-mailbox.ts +8 -18
  138. package/src/lib/server/session-reset-policy.test.ts +99 -0
  139. package/src/lib/server/session-reset-policy.ts +311 -0
  140. package/src/lib/server/session-run-manager.ts +33 -80
  141. package/src/lib/server/session-tools/autonomy-tools.test.ts +105 -0
  142. package/src/lib/server/session-tools/calendar.ts +2 -12
  143. package/src/lib/server/session-tools/connector.ts +109 -8
  144. package/src/lib/server/session-tools/context.ts +14 -2
  145. package/src/lib/server/session-tools/crawl.ts +447 -0
  146. package/src/lib/server/session-tools/crud.ts +70 -32
  147. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  148. package/src/lib/server/session-tools/delegate.ts +406 -20
  149. package/src/lib/server/session-tools/discovery.ts +22 -4
  150. package/src/lib/server/session-tools/document.ts +283 -0
  151. package/src/lib/server/session-tools/email.ts +1 -3
  152. package/src/lib/server/session-tools/extract.ts +137 -0
  153. package/src/lib/server/session-tools/file-normalize.test.ts +93 -0
  154. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  155. package/src/lib/server/session-tools/file.ts +237 -24
  156. package/src/lib/server/session-tools/human-loop.ts +227 -0
  157. package/src/lib/server/session-tools/image-gen.ts +1 -3
  158. package/src/lib/server/session-tools/index.ts +56 -1
  159. package/src/lib/server/session-tools/mailbox.ts +276 -0
  160. package/src/lib/server/session-tools/memory.ts +35 -3
  161. package/src/lib/server/session-tools/monitor.ts +150 -7
  162. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  163. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  164. package/src/lib/server/session-tools/platform.ts +142 -4
  165. package/src/lib/server/session-tools/plugin-creator.ts +86 -23
  166. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  167. package/src/lib/server/session-tools/replicate.ts +1 -3
  168. package/src/lib/server/session-tools/schedule.ts +20 -10
  169. package/src/lib/server/session-tools/session-info.ts +36 -3
  170. package/src/lib/server/session-tools/session-tools-wiring.test.ts +31 -17
  171. package/src/lib/server/session-tools/subagent.ts +193 -27
  172. package/src/lib/server/session-tools/table.ts +587 -0
  173. package/src/lib/server/session-tools/wallet.ts +13 -10
  174. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  175. package/src/lib/server/session-tools/web.ts +896 -100
  176. package/src/lib/server/storage.ts +226 -7
  177. package/src/lib/server/stream-agent-chat.ts +46 -21
  178. package/src/lib/server/structured-extract.test.ts +72 -0
  179. package/src/lib/server/structured-extract.ts +373 -0
  180. package/src/lib/server/task-mention.test.ts +16 -2
  181. package/src/lib/server/task-mention.ts +61 -10
  182. package/src/lib/server/tool-aliases.ts +44 -7
  183. package/src/lib/server/tool-capability-policy.ts +6 -0
  184. package/src/lib/server/tool-retry.ts +2 -0
  185. package/src/lib/server/watch-jobs.test.ts +173 -0
  186. package/src/lib/server/watch-jobs.ts +532 -0
  187. package/src/lib/server/ws-hub.ts +5 -3
  188. package/src/lib/validation/schemas.test.ts +26 -0
  189. package/src/lib/validation/schemas.ts +7 -0
  190. package/src/lib/ws-client.ts +14 -12
  191. package/src/proxy.ts +5 -5
  192. package/src/stores/use-app-store.ts +0 -6
  193. package/src/stores/use-chat-store.ts +31 -2
  194. package/src/types/index.ts +287 -44
  195. package/src/components/chat/new-chat-sheet.tsx +0 -253
  196. package/src/lib/server/main-session.ts +0 -17
  197. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -3,15 +3,24 @@
3
3
  import { useAppStore } from '@/stores/use-app-store'
4
4
  import { IconButton } from '@/components/shared/icon-button'
5
5
  import { NotificationCenter } from '@/components/shared/notification-center'
6
+ import { AgentAvatar } from '@/components/agents/agent-avatar'
6
7
 
7
8
  export function MobileHeader() {
8
9
  const toggleSidebar = useAppStore((s) => s.toggleSidebar)
9
10
  const currentSessionId = useAppStore((s) => s.currentSessionId)
10
11
  const sessions = useAppStore((s) => s.sessions)
12
+ const agents = useAppStore((s) => s.agents)
11
13
  const session = currentSessionId ? sessions[currentSessionId] : null
14
+ const agent = session?.agentId ? agents[session.agentId] : null
15
+ const title = agent?.name || session?.name || 'SwarmClaw'
16
+ const subtitle = agent
17
+ ? 'Agent chat'
18
+ : session
19
+ ? 'Direct chat'
20
+ : 'Workspace'
12
21
 
13
22
  return (
14
- <header className="flex items-center gap-3 px-4 py-2.5 border-b border-white/[0.04] bg-bg/80 backdrop-blur-md shrink-0 min-h-[48px]"
23
+ <header className="flex items-center gap-3 px-4 py-2.5 border-b border-white/[0.04] bg-bg/80 backdrop-blur-md shrink-0 min-h-[56px]"
15
24
  style={{ paddingTop: 'max(10px, env(safe-area-inset-top))' }}>
16
25
  <IconButton onClick={toggleSidebar} aria-label="Toggle sidebar">
17
26
  <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
@@ -20,13 +29,22 @@ export function MobileHeader() {
20
29
  <line x1="3" y1="17" x2="18" y2="17" />
21
30
  </svg>
22
31
  </IconButton>
23
- <h1 className="font-display text-[14px] font-600 tracking-[-0.02em] flex-1 truncate">
24
- {session ? (
25
- <span className="block truncate">{session.name}</span>
26
- ) : (
27
- <span className="font-700">SwarmClaw</span>
28
- )}
29
- </h1>
32
+ {agent && (
33
+ <AgentAvatar
34
+ seed={agent.avatarSeed || null}
35
+ avatarUrl={agent.avatarUrl}
36
+ name={agent.name}
37
+ size={28}
38
+ />
39
+ )}
40
+ <div className="flex-1 min-w-0">
41
+ <h1 className="font-display text-[14px] font-600 tracking-[-0.02em] truncate">
42
+ {title}
43
+ </h1>
44
+ <p className="text-[10px] text-text-3/60 truncate mt-0.5">
45
+ {subtitle}
46
+ </p>
47
+ </div>
30
48
  <NotificationCenter />
31
49
  </header>
32
50
  )
@@ -62,8 +62,8 @@ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
62
62
  }, [tab, inSidebar, loadMarketplace])
63
63
 
64
64
  const pluginList = Object.values(plugins)
65
- const corePlugins = useMemo(() => pluginList.filter((p) => p.source === 'local'), [pluginList])
66
- const extensionPlugins = useMemo(() => pluginList.filter((p) => p.source !== 'local'), [pluginList])
65
+ const corePlugins = useMemo(() => pluginList.filter((p) => p.isBuiltin), [pluginList])
66
+ const extensionPlugins = useMemo(() => pluginList.filter((p) => !p.isBuiltin), [pluginList])
67
67
 
68
68
  // Search filtering for installed plugins
69
69
  const filterInstalled = useCallback((list: PluginMeta[]) => {
@@ -293,7 +293,7 @@ function TabButton({ active, onClick, count, children }: {
293
293
  function pluginDescription(plugin: PluginMeta): string {
294
294
  const raw = (plugin.description || '').trim()
295
295
  if (raw) return raw
296
- const sourceLabel = plugin.source === 'local' ? 'core plugin' : 'installed plugin'
296
+ const sourceLabel = plugin.isBuiltin ? 'core plugin' : 'installed plugin'
297
297
  return `No description provided. Click to view metadata and controls for this ${sourceLabel}.`
298
298
  }
299
299
 
@@ -304,6 +304,7 @@ function pluginCapabilityBadges(plugin: PluginMeta): string[] {
304
304
  if (plugin.hasUI) badges.push('UI')
305
305
  if (plugin.providerCount && plugin.providerCount > 0) badges.push(`${plugin.providerCount} provider${plugin.providerCount === 1 ? '' : 's'}`)
306
306
  if (plugin.connectorCount && plugin.connectorCount > 0) badges.push(`${plugin.connectorCount} connector${plugin.connectorCount === 1 ? '' : 's'}`)
307
+ if (plugin.hasDependencyManifest) badges.push(`${plugin.dependencyCount ?? 0} dep${plugin.dependencyCount === 1 ? '' : 's'}`)
307
308
  return badges
308
309
  }
309
310
 
@@ -446,6 +447,17 @@ function PluginCard({ plugin, allowDelete, agents, onEdit, onToggle, onDelete, o
446
447
  {badge}
447
448
  </span>
448
449
  ))}
450
+ {plugin.hasDependencyManifest && (
451
+ <span className={`text-[10px] font-700 px-1.5 py-0.5 rounded-full ${
452
+ plugin.dependencyInstallStatus === 'installed'
453
+ ? 'text-emerald-400 bg-emerald-500/10'
454
+ : plugin.dependencyInstallStatus === 'error'
455
+ ? 'text-red-400 bg-red-500/10'
456
+ : 'text-amber-400 bg-amber-500/10'
457
+ }`}>
458
+ deps {plugin.dependencyInstallStatus || 'ready'}
459
+ </span>
460
+ )}
449
461
  {plugin.author && (
450
462
  <span className="text-[10px] text-text-3/40 ml-auto">
451
463
  {plugin.author}
@@ -11,7 +11,7 @@ import type { PluginMeta, PluginSettingsField, MarketplacePlugin } from '@/types
11
11
  function pluginDescription(plugin: PluginMeta): string {
12
12
  const raw = (plugin.description || '').trim()
13
13
  if (raw) return raw
14
- const sourceLabel = plugin.source === 'local' ? 'core plugin' : 'installed plugin'
14
+ const sourceLabel = plugin.isBuiltin ? 'core plugin' : 'installed plugin'
15
15
  return `No description provided. This ${sourceLabel} is available and can be configured from this panel.`
16
16
  }
17
17
 
@@ -22,6 +22,7 @@ function pluginCapabilityBadges(plugin: PluginMeta): string[] {
22
22
  if (plugin.hasUI) badges.push('UI extension')
23
23
  if (plugin.providerCount && plugin.providerCount > 0) badges.push(`${plugin.providerCount} provider${plugin.providerCount === 1 ? '' : 's'}`)
24
24
  if (plugin.connectorCount && plugin.connectorCount > 0) badges.push(`${plugin.connectorCount} connector${plugin.connectorCount === 1 ? '' : 's'}`)
25
+ if (plugin.hasDependencyManifest) badges.push(`${plugin.dependencyCount ?? 0} dep${plugin.dependencyCount === 1 ? '' : 's'}`)
25
26
  return badges
26
27
  }
27
28
 
@@ -46,8 +47,10 @@ export function PluginSheet() {
46
47
  const [activeTag, setActiveTag] = useState<string | null>(null)
47
48
  const [sort, setSort] = useState<'name' | 'downloads'>('downloads')
48
49
  const [pluginSettingsValues, setPluginSettingsValues] = useState<Record<string, unknown>>({})
50
+ const [configuredSecretFields, setConfiguredSecretFields] = useState<string[]>([])
49
51
  const [pluginSettingsLoading, setPluginSettingsLoading] = useState(false)
50
52
  const [pluginSettingsSaving, setPluginSettingsSaving] = useState(false)
53
+ const [dependencyInstalling, setDependencyInstalling] = useState(false)
51
54
 
52
55
  const editing = editingFilename ? plugins[editingFilename] : null
53
56
 
@@ -55,12 +58,19 @@ export function PluginSheet() {
55
58
  useEffect(() => {
56
59
  if (!editing?.settingsFields?.length) {
57
60
  setPluginSettingsValues({})
61
+ setConfiguredSecretFields([])
58
62
  return
59
63
  }
60
64
  setPluginSettingsLoading(true)
61
- api<Record<string, unknown>>('GET', `/plugins/settings?pluginId=${encodeURIComponent(editing.filename)}`)
62
- .then((data) => setPluginSettingsValues(data ?? {}))
63
- .catch(() => setPluginSettingsValues({}))
65
+ api<{ values?: Record<string, unknown>; configuredSecretFields?: string[] }>('GET', `/plugins/settings?pluginId=${encodeURIComponent(editing.filename)}`)
66
+ .then((data) => {
67
+ setPluginSettingsValues(data?.values ?? {})
68
+ setConfiguredSecretFields(Array.isArray(data?.configuredSecretFields) ? data.configuredSecretFields : [])
69
+ })
70
+ .catch(() => {
71
+ setPluginSettingsValues({})
72
+ setConfiguredSecretFields([])
73
+ })
64
74
  .finally(() => setPluginSettingsLoading(false))
65
75
  // eslint-disable-next-line react-hooks/exhaustive-deps
66
76
  }, [editingFilename])
@@ -69,7 +79,24 @@ export function PluginSheet() {
69
79
  if (!editing) return
70
80
  setPluginSettingsSaving(true)
71
81
  try {
72
- await api('PUT', `/plugins/settings?pluginId=${encodeURIComponent(editing.filename)}`, pluginSettingsValues)
82
+ const secretFieldSet = new Set(
83
+ (editing.settingsFields || [])
84
+ .filter((field) => field.type === 'secret')
85
+ .map((field) => field.key),
86
+ )
87
+ const payload = Object.fromEntries(
88
+ Object.entries(pluginSettingsValues).filter(([key, value]) => {
89
+ if (!secretFieldSet.has(key)) return true
90
+ return value !== undefined && value !== ''
91
+ }),
92
+ )
93
+ const response = await api<{ values?: Record<string, unknown>; configuredSecretFields?: string[] }>(
94
+ 'PUT',
95
+ `/plugins/settings?pluginId=${encodeURIComponent(editing.filename)}`,
96
+ payload,
97
+ )
98
+ setPluginSettingsValues(response?.values ?? {})
99
+ setConfiguredSecretFields(Array.isArray(response?.configuredSecretFields) ? response.configuredSecretFields : [])
73
100
  toast.success('Plugin settings saved')
74
101
  } catch (err: unknown) {
75
102
  toast.error(err instanceof Error ? err.message : 'Failed to save settings')
@@ -126,6 +153,24 @@ export function PluginSheet() {
126
153
  setDeleting(false)
127
154
  }
128
155
 
156
+ const installDependencies = useCallback(async () => {
157
+ if (!editing?.filename) return
158
+ setDependencyInstalling(true)
159
+ try {
160
+ const response = await api<{ ok: boolean }>('POST', '/plugins/dependencies', {
161
+ filename: editing.filename,
162
+ packageManager: editing.packageManager,
163
+ })
164
+ if (response?.ok) {
165
+ await loadPlugins()
166
+ toast.success('Plugin dependencies installed')
167
+ }
168
+ } catch (err: unknown) {
169
+ toast.error(err instanceof Error ? err.message : 'Failed to install plugin dependencies')
170
+ }
171
+ setDependencyInstalling(false)
172
+ }, [editing?.filename, editing?.packageManager, loadPlugins])
173
+
129
174
  const installFromMarketplace = async (p: MarketplacePlugin) => {
130
175
  setInstalling(p.id)
131
176
  const toastId = toast.loading(`Installing ${p.name}...`)
@@ -189,7 +234,9 @@ export function PluginSheet() {
189
234
  <div className="grid grid-cols-2 gap-2 mt-3">
190
235
  <div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
191
236
  <div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Source</div>
192
- <div className="text-[11px] text-text-2">{editing.source === 'local' ? 'Core Platform' : 'Extension'}</div>
237
+ <div className="text-[11px] text-text-2">
238
+ {editing.isBuiltin ? 'Core Platform' : editing.source === 'marketplace' ? 'Marketplace Extension' : 'Local Extension'}
239
+ </div>
193
240
  </div>
194
241
  <div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
195
242
  <div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Author</div>
@@ -219,6 +266,54 @@ export function PluginSheet() {
219
266
  )}
220
267
  </div>
221
268
 
269
+ {(editing.hasDependencyManifest || !editing.isBuiltin) && (
270
+ <div className="py-4 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
271
+ <div className="flex items-start justify-between gap-3">
272
+ <div>
273
+ <div className="text-[13px] font-600 text-text">Dependencies</div>
274
+ <p className="text-[11px] text-text-3/60 mt-1">
275
+ {editing.hasDependencyManifest
276
+ ? `Managed in a per-plugin workspace${editing.packageManager ? ` via ${editing.packageManager}` : ''}.`
277
+ : 'No package.json manifest is currently attached to this plugin.'}
278
+ </p>
279
+ </div>
280
+ {editing.hasDependencyManifest && (
281
+ <button
282
+ onClick={() => { void installDependencies() }}
283
+ disabled={dependencyInstalling}
284
+ className="px-3 py-2 rounded-[10px] bg-accent-soft text-[11px] font-700 text-accent-bright hover:bg-accent-bright/15 transition-all cursor-pointer border-none disabled:opacity-50"
285
+ style={{ fontFamily: 'inherit' }}
286
+ >
287
+ {dependencyInstalling ? 'Installing…' : 'Install / Refresh'}
288
+ </button>
289
+ )}
290
+ </div>
291
+
292
+ <div className="grid grid-cols-2 gap-2 mt-3">
293
+ <div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
294
+ <div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Runtime deps</div>
295
+ <div className="text-[11px] text-text-2">{editing.dependencyCount ?? 0}</div>
296
+ </div>
297
+ <div className="rounded-[10px] bg-bg/50 border border-white/[0.05] px-2.5 py-2">
298
+ <div className="text-[10px] uppercase tracking-[0.08em] text-text-3/60 mb-0.5">Status</div>
299
+ <div className="text-[11px] text-text-2">{editing.dependencyInstallStatus || 'none'}</div>
300
+ </div>
301
+ </div>
302
+
303
+ {editing.dependencyInstallError && (
304
+ <div className="mt-3 p-2.5 rounded-[10px] bg-red-500/[0.08] border border-red-500/20 text-[11px] text-red-300/90">
305
+ {editing.dependencyInstallError}
306
+ </div>
307
+ )}
308
+
309
+ {editing.dependencyInstalledAt && (
310
+ <p className="text-[10px] text-text-3/45 mt-3">
311
+ Last installed {new Date(editing.dependencyInstalledAt).toLocaleString()}
312
+ </p>
313
+ )}
314
+ </div>
315
+ )}
316
+
222
317
  <div className="flex items-center justify-between py-3 px-4 rounded-[14px] bg-surface border border-white/[0.06]">
223
318
  <div>
224
319
  <span className="text-[13px] font-600 text-text block">Enabled</span>
@@ -246,6 +341,7 @@ export function PluginSheet() {
246
341
  key={field.key}
247
342
  field={field}
248
343
  value={pluginSettingsValues[field.key]}
344
+ configured={configuredSecretFields.includes(field.key)}
249
345
  onChange={(v) => setPluginSettingsValues((prev) => ({ ...prev, [field.key]: v }))}
250
346
  />
251
347
  ))}
@@ -442,7 +538,7 @@ export function PluginSheet() {
442
538
  </p>
443
539
  )}
444
540
  <p className="text-[10px] text-text-3/60 mt-3">
445
- Works with SwarmClaw and OpenClaw plugin formats. URL must be HTTPS.
541
+ Works with `.js` / `.mjs` SwarmClaw and OpenClaw plugin formats. URL must be HTTPS.
446
542
  </p>
447
543
  </div>
448
544
  )}
@@ -465,7 +561,17 @@ export function PluginSheet() {
465
561
  )
466
562
  }
467
563
 
468
- function PluginSettingRow({ field, value, onChange }: { field: PluginSettingsField; value: unknown; onChange: (v: unknown) => void }) {
564
+ function PluginSettingRow({
565
+ field,
566
+ value,
567
+ configured,
568
+ onChange,
569
+ }: {
570
+ field: PluginSettingsField
571
+ value: unknown
572
+ configured: boolean
573
+ onChange: (v: unknown) => void
574
+ }) {
469
575
  const inputCls = 'w-full py-2 px-3 rounded-[8px] text-[12px] bg-bg border border-white/[0.06] text-text placeholder:text-text-3/50 outline-none focus:border-accent-bright/30'
470
576
 
471
577
  return (
@@ -509,11 +615,14 @@ function PluginSettingRow({ field, value, onChange }: { field: PluginSettingsFie
509
615
  type={field.type === 'secret' ? 'password' : 'text'}
510
616
  value={String(value ?? field.defaultValue ?? '')}
511
617
  onChange={(e) => onChange(e.target.value || undefined)}
512
- placeholder={field.placeholder}
618
+ placeholder={field.type === 'secret' && configured ? 'Stored securely. Enter a new value to replace it.' : field.placeholder}
513
619
  className={inputCls}
514
620
  style={{ fontFamily: 'inherit' }}
515
621
  />
516
622
  )}
623
+ {field.type === 'secret' && configured && (
624
+ <p className="text-[10px] text-emerald-400/90 mt-1">Stored securely. Leave blank to keep the current value.</p>
625
+ )}
517
626
  {field.help && <p className="text-[10px] text-text-3/60 mt-1">{field.help}</p>}
518
627
  </div>
519
628
  )
@@ -102,6 +102,7 @@ export function ProjectDetail() {
102
102
  const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
103
103
 
104
104
  const [assignPickerOpen, setAssignPickerOpen] = useState(false)
105
+ const now = Date.now()
105
106
 
106
107
  const project = activeProjectFilter ? projects[activeProjectFilter] : null
107
108
 
@@ -150,6 +151,93 @@ export function ProjectDetail() {
150
151
  return items.sort((a, b) => b.time - a.time).slice(0, 12)
151
152
  }, [projectTasks, projectSchedules, projectAgents])
152
153
 
154
+ const approvalTasks = useMemo(
155
+ () => projectTasks.filter((task) => !!task.pendingApproval),
156
+ [projectTasks],
157
+ )
158
+
159
+ const blockedTasks = useMemo(
160
+ () => projectTasks.filter((task) => (task.blockedBy?.length || 0) > 0),
161
+ [projectTasks],
162
+ )
163
+
164
+ const overdueTasks = useMemo(
165
+ () => projectTasks.filter((task) => !!task.dueAt && task.dueAt < now && task.status !== 'completed' && task.status !== 'archived'),
166
+ [now, projectTasks],
167
+ )
168
+
169
+ const staleTasks = useMemo(
170
+ () => projectTasks.filter((task) => task.status !== 'completed' && task.status !== 'archived' && now - task.updatedAt > 3 * 24 * 60 * 60 * 1000),
171
+ [now, projectTasks],
172
+ )
173
+
174
+ const overdueSchedules = useMemo(
175
+ () => projectSchedules.filter((schedule) => schedule.status === 'active' && !!schedule.nextRunAt && schedule.nextRunAt < now),
176
+ [now, projectSchedules],
177
+ )
178
+
179
+ const nextScheduledRun = useMemo(
180
+ () => projectSchedules
181
+ .filter((schedule) => schedule.status === 'active' && !!schedule.nextRunAt && schedule.nextRunAt >= now)
182
+ .sort((a, b) => (a.nextRunAt || 0) - (b.nextRunAt || 0))[0] || null,
183
+ [now, projectSchedules],
184
+ )
185
+
186
+ const busiestAgent = useMemo(() => {
187
+ return projectAgents
188
+ .map((agent) => ({
189
+ agent,
190
+ taskCount: projectTasks.filter((task) => task.agentId === agent.id && task.status !== 'completed' && task.status !== 'archived').length,
191
+ }))
192
+ .sort((a, b) => b.taskCount - a.taskCount)[0] || null
193
+ }, [projectAgents, projectTasks])
194
+
195
+ const attentionItems = useMemo(() => {
196
+ const seen = new Set<string>()
197
+ const items: Array<{ id: string; label: string; detail: string; tone: string; onClick: () => void }> = []
198
+ for (const task of approvalTasks.slice(0, 2)) {
199
+ seen.add(task.id)
200
+ items.push({
201
+ id: `approval-${task.id}`,
202
+ label: task.title,
203
+ detail: 'Awaiting tool approval',
204
+ tone: 'text-amber-400',
205
+ onClick: () => { setEditingTaskId(task.id); setTaskSheetOpen(true) },
206
+ })
207
+ }
208
+ for (const task of blockedTasks.slice(0, 2)) {
209
+ if (seen.has(task.id)) continue
210
+ seen.add(task.id)
211
+ items.push({
212
+ id: `blocked-${task.id}`,
213
+ label: task.title,
214
+ detail: `${task.blockedBy?.length || 0} blocker${task.blockedBy?.length === 1 ? '' : 's'}`,
215
+ tone: 'text-rose-400',
216
+ onClick: () => { setEditingTaskId(task.id); setTaskSheetOpen(true) },
217
+ })
218
+ }
219
+ if (staleTasks[0] && !seen.has(staleTasks[0].id)) {
220
+ seen.add(staleTasks[0].id)
221
+ items.push({
222
+ id: `stale-${staleTasks[0].id}`,
223
+ label: staleTasks[0].title,
224
+ detail: 'No recent progress in 3+ days',
225
+ tone: 'text-sky-400',
226
+ onClick: () => { setEditingTaskId(staleTasks[0].id); setTaskSheetOpen(true) },
227
+ })
228
+ }
229
+ if (overdueSchedules[0]) {
230
+ items.push({
231
+ id: `schedule-${overdueSchedules[0].id}`,
232
+ label: overdueSchedules[0].name,
233
+ detail: 'Schedule missed its next run',
234
+ tone: 'text-red-400',
235
+ onClick: () => { setEditingScheduleId(overdueSchedules[0].id); setScheduleSheetOpen(true) },
236
+ })
237
+ }
238
+ return items.slice(0, 5)
239
+ }, [approvalTasks, blockedTasks, overdueSchedules, setEditingScheduleId, setEditingTaskId, setScheduleSheetOpen, setTaskSheetOpen, staleTasks])
240
+
153
241
  const handleUnassignAgent = async (agentId: string) => {
154
242
  await updateAgent(agentId, { projectId: undefined })
155
243
  await loadAgents()
@@ -212,6 +300,101 @@ export function ProjectDetail() {
212
300
  </div>
213
301
  </div>
214
302
 
303
+ <div className="grid grid-cols-1 lg:grid-cols-[minmax(0,1fr)_280px] gap-4 mb-8">
304
+ <div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] px-5 py-5">
305
+ <div className="flex items-center justify-between gap-4 mb-4">
306
+ <div>
307
+ <h2 className="font-display text-[18px] font-700 tracking-[-0.02em] text-text">Project Health</h2>
308
+ <p className="text-[12px] text-text-3/60 mt-1">Surface blockers and the next useful action before digging into the full history.</p>
309
+ </div>
310
+ <button
311
+ onClick={() => setActiveView('tasks')}
312
+ className="shrink-0 px-3 py-2 rounded-[10px] bg-white/[0.04] text-[12px] font-600 text-text-2 hover:bg-white/[0.08] transition-all cursor-pointer border-none"
313
+ style={{ fontFamily: 'inherit' }}
314
+ >
315
+ Open Task Board
316
+ </button>
317
+ </div>
318
+
319
+ <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
320
+ {[
321
+ { label: 'Awaiting Approval', value: approvalTasks.length, tone: 'text-amber-400', hint: 'Tasks paused on approval' },
322
+ { label: 'Blocked', value: blockedTasks.length, tone: 'text-rose-400', hint: 'Tasks waiting on dependencies' },
323
+ { label: 'Overdue', value: overdueTasks.length + overdueSchedules.length, tone: 'text-red-400', hint: 'Tasks or schedules behind plan' },
324
+ { label: 'Stale Tasks', value: staleTasks.length, tone: 'text-sky-400', hint: 'No meaningful progress in 3+ days' },
325
+ ].map((item) => (
326
+ <div key={item.label} className="rounded-[12px] border border-white/[0.06] bg-surface/60 px-4 py-3">
327
+ <div className={`text-[22px] font-display font-700 tracking-[-0.02em] ${item.tone}`}>{item.value}</div>
328
+ <div className="text-[11px] font-600 text-text-2 mt-0.5">{item.label}</div>
329
+ <p className="text-[10px] text-text-3/45 mt-1 leading-relaxed">{item.hint}</p>
330
+ </div>
331
+ ))}
332
+ </div>
333
+
334
+ <div className="flex flex-wrap gap-2 mt-4">
335
+ {busiestAgent?.agent && (
336
+ <button
337
+ onClick={() => { setCurrentAgent(busiestAgent.agent.id); setActiveView('agents') }}
338
+ className="inline-flex items-center gap-2 px-3 py-2 rounded-[10px] bg-accent-soft text-[12px] font-600 text-accent-bright hover:bg-accent-bright/15 transition-all cursor-pointer border-none"
339
+ style={{ fontFamily: 'inherit' }}
340
+ >
341
+ <AgentAvatar seed={busiestAgent.agent.avatarSeed} avatarUrl={busiestAgent.agent.avatarUrl} name={busiestAgent.agent.name} size={18} />
342
+ Open busiest agent
343
+ </button>
344
+ )}
345
+ {(approvalTasks[0] || blockedTasks[0] || overdueTasks[0]) && (
346
+ <button
347
+ onClick={() => {
348
+ const nextTask = approvalTasks[0] || blockedTasks[0] || overdueTasks[0]
349
+ if (!nextTask) return
350
+ setEditingTaskId(nextTask.id)
351
+ setTaskSheetOpen(true)
352
+ }}
353
+ className="px-3 py-2 rounded-[10px] bg-white/[0.04] text-[12px] font-600 text-text-2 hover:bg-white/[0.08] transition-all cursor-pointer border-none"
354
+ style={{ fontFamily: 'inherit' }}
355
+ >
356
+ Review next task
357
+ </button>
358
+ )}
359
+ {nextScheduledRun && (
360
+ <button
361
+ onClick={() => { setEditingScheduleId(nextScheduledRun.id); setScheduleSheetOpen(true) }}
362
+ className="px-3 py-2 rounded-[10px] bg-white/[0.04] text-[12px] font-600 text-text-2 hover:bg-white/[0.08] transition-all cursor-pointer border-none"
363
+ style={{ fontFamily: 'inherit' }}
364
+ >
365
+ Next run {relativeDate(nextScheduledRun.nextRunAt || now)}
366
+ </button>
367
+ )}
368
+ </div>
369
+ </div>
370
+
371
+ <div className="rounded-[16px] border border-white/[0.06] bg-white/[0.02] px-5 py-5">
372
+ <div className="flex items-center justify-between mb-3">
373
+ <h3 className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">Needs Attention</h3>
374
+ <span className="text-[11px] text-text-3/40">{attentionItems.length || 0} items</span>
375
+ </div>
376
+ {attentionItems.length === 0 ? (
377
+ <div className="rounded-[12px] border border-dashed border-white/[0.08] px-4 py-6 text-center">
378
+ <p className="text-[12px] text-text-3/45">No urgent blockers right now.</p>
379
+ </div>
380
+ ) : (
381
+ <div className="flex flex-col gap-2">
382
+ {attentionItems.map((item) => (
383
+ <button
384
+ key={item.id}
385
+ onClick={item.onClick}
386
+ className="w-full rounded-[12px] border border-white/[0.06] bg-surface/60 px-3.5 py-3 text-left hover:bg-white/[0.04] transition-all cursor-pointer"
387
+ style={{ fontFamily: 'inherit' }}
388
+ >
389
+ <div className={`text-[11px] font-700 uppercase tracking-[0.08em] ${item.tone}`}>{item.detail}</div>
390
+ <div className="text-[13px] font-600 text-text mt-1 line-clamp-2">{item.label}</div>
391
+ </button>
392
+ ))}
393
+ </div>
394
+ )}
395
+ </div>
396
+ </div>
397
+
215
398
  {/* Stats cards */}
216
399
  <div className="grid grid-cols-4 gap-3 mb-8">
217
400
  {[
@@ -12,7 +12,7 @@ interface Props {
12
12
  onSelect: (agentId: string) => void
13
13
  /** Show a "None" option at the top for optional single-select */
14
14
  noneOption?: { label: string; onSelect: () => void }
15
- /** Show orchestrator badge */
15
+ /** Show delegation-capable badge */
16
16
  showOrchBadge?: boolean
17
17
  /** Max height of the scrollable list */
18
18
  maxHeight?: number
@@ -76,7 +76,7 @@ export function AgentPickerList({
76
76
  <span className={`text-[13px] font-600 flex-1 truncate ${active ? 'text-accent-bright' : 'text-text-2'}`}>
77
77
  {a.name}
78
78
  </span>
79
- {showOrchBadge && a.isOrchestrator && (
79
+ {showOrchBadge && a.platformAssignScope === 'all' && (
80
80
  <span className="text-[10px] text-text-3/60 flex items-center gap-0.5">
81
81
  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><path d="M16 3h5v5"/><path d="M21 3l-7 7"/><path d="M8 21H3v-5"/><path d="M3 21l7-7"/></svg>
82
82
  </span>