@swarmclawai/swarmclaw 0.6.8 → 0.7.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 (166) hide show
  1. package/README.md +70 -45
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useState, useRef } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { api } from '@/lib/api-client'
6
+ import { toast } from 'sonner'
6
7
 
7
8
  const transportColors: Record<string, string> = {
8
9
  stdio: 'bg-emerald-500/15 text-emerald-400',
@@ -11,6 +12,39 @@ const transportColors: Record<string, string> = {
11
12
  }
12
13
 
13
14
  type McpStatus = { ok: boolean; tools?: string[]; error?: string; loading: boolean }
15
+ type McpToolMeta = { name: string; description?: string; inputSchema?: Record<string, unknown> }
16
+ type McpInvokeResult = { ok: boolean; text?: string; error?: string; isError?: boolean; result?: unknown }
17
+ type McpConformanceIssue = { level: 'error' | 'warning'; code: string; message: string; toolName?: string }
18
+ type McpConformanceResult = {
19
+ ok: boolean
20
+ toolsCount: number
21
+ smokeToolName: string | null
22
+ issues: McpConformanceIssue[]
23
+ timings: { connectMs: number; listToolsMs: number; smokeInvokeMs: number | null }
24
+ }
25
+
26
+ function buildArgsTemplate(inputSchema: Record<string, unknown> | undefined): string {
27
+ const schema = inputSchema || {}
28
+ const required = Array.isArray(schema.required) ? schema.required.filter((k): k is string => typeof k === 'string') : []
29
+ const properties = (schema.properties && typeof schema.properties === 'object')
30
+ ? schema.properties as Record<string, Record<string, unknown>>
31
+ : {}
32
+ const template: Record<string, unknown> = {}
33
+ for (const key of required.slice(0, 8)) {
34
+ const prop = properties[key] || {}
35
+ const type = typeof prop.type === 'string' ? prop.type : 'string'
36
+ template[key] = type === 'number' || type === 'integer'
37
+ ? 0
38
+ : type === 'boolean'
39
+ ? false
40
+ : type === 'array'
41
+ ? []
42
+ : type === 'object'
43
+ ? {}
44
+ : ''
45
+ }
46
+ return JSON.stringify(template, null, 2) || '{}'
47
+ }
14
48
 
15
49
  export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
16
50
  const mcpServers = useAppStore((s) => s.mcpServers)
@@ -18,13 +52,35 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
18
52
  const setMcpServerSheetOpen = useAppStore((s) => s.setMcpServerSheetOpen)
19
53
  const setEditingMcpServerId = useAppStore((s) => s.setEditingMcpServerId)
20
54
  const [statuses, setStatuses] = useState<Record<string, McpStatus>>({})
55
+ const [inspectorServerId, setInspectorServerId] = useState<string | null>(null)
56
+ const [toolsByServer, setToolsByServer] = useState<Record<string, McpToolMeta[]>>({})
57
+ const [inspectorLoading, setInspectorLoading] = useState(false)
58
+ const [inspectorError, setInspectorError] = useState<string | null>(null)
59
+ const [selectedTool, setSelectedTool] = useState('')
60
+ const [argsJson, setArgsJson] = useState('{}')
61
+ const [invokeLoading, setInvokeLoading] = useState(false)
62
+ const [invokeResult, setInvokeResult] = useState<McpInvokeResult | null>(null)
63
+ const [conformanceByServer, setConformanceByServer] = useState<Record<string, McpConformanceResult>>({})
64
+ const [conformanceLoading, setConformanceLoading] = useState<Record<string, boolean>>({})
21
65
  const timersRef = useRef<ReturnType<typeof setTimeout>[]>([])
22
66
 
23
67
  useEffect(() => {
24
68
  loadMcpServers()
25
69
  }, [loadMcpServers])
26
70
 
71
+ useEffect(() => {
72
+ if (inspectorServerId && !mcpServers[inspectorServerId]) {
73
+ setInspectorServerId(null)
74
+ setInspectorError(null)
75
+ setInvokeResult(null)
76
+ }
77
+ }, [inspectorServerId, mcpServers])
78
+
27
79
  const serverList = Object.values(mcpServers)
80
+ const activeInspectorServer = inspectorServerId ? mcpServers[inspectorServerId] : null
81
+ const activeTools = inspectorServerId ? (toolsByServer[inspectorServerId] || []) : []
82
+ const activeToolMeta = activeTools.find((tool) => tool.name === selectedTool) || null
83
+ const activeConformance = inspectorServerId ? conformanceByServer[inspectorServerId] : null
28
84
 
29
85
  // Staggered status tests on mount
30
86
  useEffect(() => {
@@ -55,8 +111,16 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
55
111
 
56
112
  const handleDelete = async (e: React.MouseEvent, id: string) => {
57
113
  e.stopPropagation()
58
- await api('DELETE', `/mcp-servers/${id}`)
59
- await loadMcpServers()
114
+ const server = mcpServers[id]
115
+ if (!confirm(`Delete MCP server "${server?.name || id}"?`)) return
116
+
117
+ try {
118
+ await api('DELETE', `/mcp-servers/${id}`)
119
+ toast.success('MCP server deleted')
120
+ await loadMcpServers()
121
+ } catch (err: unknown) {
122
+ toast.error(err instanceof Error ? err.message : 'Failed to delete server')
123
+ }
60
124
  }
61
125
 
62
126
  const handleRetest = async (e: React.MouseEvent, id: string) => {
@@ -65,8 +129,117 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
65
129
  try {
66
130
  const res = await api<{ ok: boolean; tools?: string[]; error?: string }>('POST', `/mcp-servers/${id}/test`)
67
131
  setStatuses((prev) => ({ ...prev, [id]: { ok: res.ok, tools: res.tools, error: res.error, loading: false } }))
68
- } catch {
132
+ if (res.ok) toast.success('Connection test passed')
133
+ else toast.error(res.error || 'Connection test failed')
134
+ } catch (err: unknown) {
69
135
  setStatuses((prev) => ({ ...prev, [id]: { ok: false, error: 'Test failed', loading: false } }))
136
+ toast.error(err instanceof Error ? err.message : 'Test failed')
137
+ }
138
+ }
139
+
140
+ const handleConformance = async (e: React.MouseEvent, id: string) => {
141
+ e.stopPropagation()
142
+ setConformanceLoading((prev) => ({ ...prev, [id]: true }))
143
+ try {
144
+ const res = await api<McpConformanceResult>('POST', `/mcp-servers/${id}/conformance`, {
145
+ timeoutMs: 12000,
146
+ })
147
+ setConformanceByServer((prev) => ({ ...prev, [id]: res }))
148
+ if (res.ok) toast.success('Conformance check passed')
149
+ else toast.error(`Conformance issues found (${res.issues.length})`)
150
+ } catch (err) {
151
+ const msg = err instanceof Error ? err.message : 'Conformance failed'
152
+ setConformanceByServer((prev) => ({
153
+ ...prev,
154
+ [id]: {
155
+ ok: false,
156
+ toolsCount: 0,
157
+ smokeToolName: null,
158
+ issues: [{ level: 'error', code: 'request_failed', message: msg }],
159
+ timings: { connectMs: 0, listToolsMs: 0, smokeInvokeMs: null },
160
+ },
161
+ }))
162
+ toast.error(msg)
163
+ } finally {
164
+ setConformanceLoading((prev) => ({ ...prev, [id]: false }))
165
+ }
166
+ }
167
+
168
+ const openInspector = async (e: React.MouseEvent, id: string) => {
169
+ e.stopPropagation()
170
+ if (inspectorServerId === id) {
171
+ setInspectorServerId(null)
172
+ setInspectorError(null)
173
+ return
174
+ }
175
+ setInspectorServerId(id)
176
+ setInspectorError(null)
177
+ setInvokeResult(null)
178
+
179
+ if (toolsByServer[id]?.length) {
180
+ const first = toolsByServer[id][0]
181
+ setSelectedTool(first.name)
182
+ setArgsJson(buildArgsTemplate(first.inputSchema))
183
+ return
184
+ }
185
+
186
+ setInspectorLoading(true)
187
+ try {
188
+ const tools = await api<McpToolMeta[]>('GET', `/mcp-servers/${id}/tools`)
189
+ setToolsByServer((prev) => ({ ...prev, [id]: Array.isArray(tools) ? tools : [] }))
190
+ const first = Array.isArray(tools) && tools.length > 0 ? tools[0] : null
191
+ setSelectedTool(first?.name || '')
192
+ setArgsJson(first ? buildArgsTemplate(first.inputSchema) : '{}')
193
+ } catch (err) {
194
+ setInspectorError(err instanceof Error ? err.message : 'Failed to load tools')
195
+ setSelectedTool('')
196
+ setArgsJson('{}')
197
+ } finally {
198
+ setInspectorLoading(false)
199
+ }
200
+ }
201
+
202
+ const handleToolChange = (toolName: string) => {
203
+ setSelectedTool(toolName)
204
+ setInvokeResult(null)
205
+ const tool = activeTools.find((t) => t.name === toolName)
206
+ setArgsJson(buildArgsTemplate(tool?.inputSchema))
207
+ }
208
+
209
+ const handleInvoke = async () => {
210
+ if (!inspectorServerId || !selectedTool) return
211
+ let parsedArgs: Record<string, unknown> = {}
212
+ try {
213
+ parsedArgs = argsJson.trim() ? JSON.parse(argsJson) : {}
214
+ if (!parsedArgs || typeof parsedArgs !== 'object' || Array.isArray(parsedArgs)) {
215
+ setInvokeResult({ ok: false, error: 'Args must be a JSON object.' })
216
+ return
217
+ }
218
+ } catch {
219
+ setInvokeResult({ ok: false, error: 'Args must be valid JSON.' })
220
+ return
221
+ }
222
+
223
+ setInvokeLoading(true)
224
+ setInvokeResult(null)
225
+ try {
226
+ const result = await api<McpInvokeResult>('POST', `/mcp-servers/${inspectorServerId}/invoke`, {
227
+ toolName: selectedTool,
228
+ args: parsedArgs,
229
+ })
230
+ setInvokeResult(result)
231
+ if (result.ok) {
232
+ if (result.isError) toast.error('Tool returned an error')
233
+ else toast.success('Tool invoked successfully')
234
+ } else {
235
+ toast.error(result.error || 'Invocation failed')
236
+ }
237
+ } catch (err) {
238
+ const msg = err instanceof Error ? err.message : 'Invocation failed'
239
+ setInvokeResult({ ok: false, error: msg })
240
+ toast.error(msg)
241
+ } finally {
242
+ setInvokeLoading(false)
70
243
  }
71
244
  }
72
245
 
@@ -84,60 +257,189 @@ export function McpServerList({ inSidebar }: { inSidebar?: boolean }) {
84
257
  </button>
85
258
  </div>
86
259
  ) : (
87
- <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
88
- {serverList.map((server) => (
89
- <button
90
- key={server.id}
91
- onClick={() => handleEdit(server.id)}
92
- className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
93
- >
94
- <div className="flex items-center justify-between mb-1">
95
- <div className="flex items-center gap-2 min-w-0">
96
- {(() => {
97
- const s = statuses[server.id]
98
- if (!s || s.loading) return <span className="w-2 h-2 rounded-full bg-yellow-400 animate-pulse shrink-0" title="Testing..." />
99
- if (s.ok) return (
100
- <span className="flex items-center gap-1 shrink-0">
101
- <span className="w-2 h-2 rounded-full bg-emerald-400 shrink-0" />
102
- {s.tools && <span className="text-[10px] text-emerald-400/80 font-mono">{s.tools.length} tools</span>}
103
- </span>
104
- )
105
- return <span className="w-2 h-2 rounded-full bg-red-400 shrink-0" title={s.error || 'Failed'} />
106
- })()}
107
- <span className="font-display text-[14px] font-600 text-text truncate">{server.name}</span>
260
+ <>
261
+ {!inSidebar && inspectorServerId && (
262
+ <div className="mb-4 p-4 rounded-[14px] border border-white/[0.08] bg-surface-2">
263
+ <div className="flex items-center justify-between gap-3 mb-3">
264
+ <div className="min-w-0">
265
+ <h3 className="font-display text-[14px] font-600 text-text truncate">
266
+ MCP Inspector: {activeInspectorServer?.name || inspectorServerId}
267
+ </h3>
268
+ <p className="text-[12px] text-text-3/70">List tools and invoke them with structured JSON args.</p>
269
+ </div>
270
+ <button
271
+ onClick={() => setInspectorServerId(null)}
272
+ className="text-[11px] text-text-3/70 hover:text-text-2 transition-colors"
273
+ >
274
+ Close
275
+ </button>
276
+ </div>
277
+
278
+ {inspectorLoading ? (
279
+ <p className="text-[12px] text-text-3/70">Loading tools...</p>
280
+ ) : inspectorError ? (
281
+ <p className="text-[12px] text-red-300">{inspectorError}</p>
282
+ ) : (
283
+ <div className="space-y-3">
284
+ {activeConformance && (
285
+ <div className={`rounded-[10px] border p-3 ${activeConformance.ok ? 'border-emerald-400/20 bg-emerald-500/[0.06]' : 'border-amber-400/20 bg-amber-500/[0.06]'}`}>
286
+ <p className={`text-[12px] font-600 mb-1 ${activeConformance.ok ? 'text-emerald-300' : 'text-amber-300'}`}>
287
+ Conformance {activeConformance.ok ? 'passed' : 'issues found'}
288
+ </p>
289
+ <p className="text-[11px] text-text-2/80">
290
+ tools={activeConformance.toolsCount}, smoke={activeConformance.smokeToolName || 'none'}, issues={activeConformance.issues.length}
291
+ </p>
292
+ </div>
293
+ )}
294
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
295
+ <label className="text-[11px] text-text-3/70 uppercase tracking-[0.08em]">Tool</label>
296
+ <label className="text-[11px] text-text-3/70 uppercase tracking-[0.08em]">Args (JSON)</label>
297
+ <select
298
+ value={selectedTool}
299
+ onChange={(e) => handleToolChange(e.target.value)}
300
+ className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-bg text-text text-[12px]"
301
+ style={{ fontFamily: 'inherit' }}
302
+ >
303
+ {activeTools.length === 0 && <option value="">No tools available</option>}
304
+ {activeTools.map((tool) => (
305
+ <option key={tool.name} value={tool.name}>{tool.name}</option>
306
+ ))}
307
+ </select>
308
+ <textarea
309
+ value={argsJson}
310
+ onChange={(e) => setArgsJson(e.target.value)}
311
+ className="min-h-[96px] px-3 py-2 rounded-[10px] border border-white/[0.08] bg-bg text-text text-[12px] font-mono"
312
+ />
313
+ </div>
314
+
315
+ {activeToolMeta?.description && (
316
+ <p className="text-[12px] text-text-3/80">{activeToolMeta.description}</p>
317
+ )}
318
+
319
+ <div className="flex items-center gap-2">
320
+ <button
321
+ onClick={handleInvoke}
322
+ disabled={!selectedTool || invokeLoading}
323
+ className="px-3 py-1.5 rounded-[9px] bg-accent-soft text-accent-bright text-[12px] font-600 disabled:opacity-60 disabled:cursor-not-allowed"
324
+ style={{ fontFamily: 'inherit' }}
325
+ >
326
+ {invokeLoading ? 'Running...' : 'Invoke Plugin'} </button>
327
+ <span className="text-[11px] text-text-3/60">Result is captured below with raw payload.</span>
328
+ </div>
329
+
330
+ {invokeResult && (
331
+ <div className={`rounded-[10px] border p-3 ${invokeResult.ok ? 'border-emerald-400/20 bg-emerald-500/[0.06]' : 'border-red-400/20 bg-red-500/[0.06]'}`}>
332
+ <p className={`text-[12px] font-600 mb-2 ${invokeResult.ok ? 'text-emerald-300' : 'text-red-300'}`}>
333
+ {invokeResult.ok ? (invokeResult.isError ? 'Invocation returned MCP error' : 'Invocation succeeded') : 'Invocation failed'}
334
+ </p>
335
+ <pre className="text-[11px] text-text-2/90 font-mono whitespace-pre-wrap break-words">
336
+ {invokeResult.ok
337
+ ? JSON.stringify({ text: invokeResult.text, isError: invokeResult.isError, result: invokeResult.result }, null, 2)
338
+ : (invokeResult.error || 'Unknown error')}
339
+ </pre>
340
+ </div>
341
+ )}
108
342
  </div>
109
- <div className="flex items-center gap-2 shrink-0 ml-2">
110
- {!inSidebar && (
343
+ )}
344
+ </div>
345
+ )}
346
+
347
+ <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
348
+ {serverList.map((server) => (
349
+ <div
350
+ key={server.id}
351
+ role="button"
352
+ tabIndex={0}
353
+ onClick={() => handleEdit(server.id)}
354
+ onKeyDown={(e) => {
355
+ if (e.key === 'Enter' || e.key === ' ') {
356
+ e.preventDefault()
357
+ handleEdit(server.id)
358
+ }
359
+ }}
360
+ className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
361
+ >
362
+ <div className="flex items-center justify-between mb-1">
363
+ <div className="flex items-center gap-2 min-w-0">
364
+ {(() => {
365
+ const s = statuses[server.id]
366
+ if (!s || s.loading) return <span className="w-2 h-2 rounded-full bg-yellow-400 animate-pulse shrink-0" title="Testing..." />
367
+ if (s.ok) return (
368
+ <span className="flex items-center gap-1 shrink-0">
369
+ <span className="w-2 h-2 rounded-full bg-emerald-400 shrink-0" />
370
+ {s.tools && <span className="text-[10px] text-emerald-400/80 font-mono">{s.tools.length} tools</span>}
371
+ </span>
372
+ )
373
+ return <span className="w-2 h-2 rounded-full bg-red-400 shrink-0" title={s.error || 'Failed'} />
374
+ })()}
375
+ <span className="font-display text-[14px] font-600 text-text truncate">{server.name}</span>
376
+ </div>
377
+ <div className="flex items-center gap-2 shrink-0 ml-2">
378
+ {!inSidebar && (
379
+ <>
380
+ <button
381
+ onClick={(e) => openInspector(e, server.id)}
382
+ className={`text-[10px] font-600 px-2 py-0.5 rounded-[7px] transition-colors ${
383
+ inspectorServerId === server.id
384
+ ? 'bg-accent-soft text-accent-bright'
385
+ : 'bg-white/[0.06] text-text-3 hover:text-text-2'
386
+ }`}
387
+ title="Open MCP inspector"
388
+ >
389
+ Inspect
390
+ </button>
391
+ <button
392
+ onClick={(e) => handleConformance(e, server.id)}
393
+ className={`text-[10px] font-600 px-2 py-0.5 rounded-[7px] transition-colors ${
394
+ conformanceByServer[server.id]?.ok
395
+ ? 'bg-emerald-500/10 text-emerald-300'
396
+ : conformanceByServer[server.id]
397
+ ? 'bg-amber-500/10 text-amber-300'
398
+ : 'bg-white/[0.06] text-text-3 hover:text-text-2'
399
+ }`}
400
+ title="Run MCP conformance checks"
401
+ >
402
+ {conformanceLoading[server.id] ? 'Checking...' : 'Conformance'}
403
+ </button>
404
+ <button
405
+ onClick={(e) => handleRetest(e, server.id)}
406
+ className="text-text-3/40 hover:text-text-2 transition-colors p-0.5"
407
+ title="Re-test connection"
408
+ >
409
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
410
+ <path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16" />
411
+ </svg>
412
+ </button>
413
+ </>
414
+ )}
415
+ <span className={`text-[10px] font-mono px-2 py-0.5 rounded-full ${transportColors[server.transport] || 'bg-white/10 text-text-3'}`}>
416
+ {server.transport}
417
+ </span>
111
418
  <button
112
- onClick={(e) => handleRetest(e, server.id)}
113
- className="text-text-3/40 hover:text-text-2 transition-colors p-0.5"
114
- title="Re-test connection"
419
+ onClick={(e) => handleDelete(e, server.id)}
420
+ className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
421
+ title="Delete server"
115
422
  >
116
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
117
- <path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16" />
423
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
424
+ <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
118
425
  </svg>
119
426
  </button>
120
- )}
121
- <span className={`text-[10px] font-mono px-2 py-0.5 rounded-full ${transportColors[server.transport] || 'bg-white/10 text-text-3'}`}>
122
- {server.transport}
123
- </span>
124
- <button
125
- onClick={(e) => handleDelete(e, server.id)}
126
- className="text-text-3/40 hover:text-red-400 transition-colors p-0.5"
127
- title="Delete server"
128
- >
129
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
130
- <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
131
- </svg>
132
- </button>
427
+ </div>
133
428
  </div>
429
+ <p className="text-[12px] text-text-3/60 font-mono truncate">
430
+ {server.transport === 'stdio' ? server.command : server.url}
431
+ </p>
432
+ {conformanceByServer[server.id] && (
433
+ <p className={`mt-1 text-[11px] ${conformanceByServer[server.id].ok ? 'text-emerald-300/80' : 'text-amber-300/80'}`}>
434
+ {conformanceByServer[server.id].ok
435
+ ? `Conformance passed (${conformanceByServer[server.id].toolsCount} tools)`
436
+ : `Conformance issues: ${conformanceByServer[server.id].issues.length}`}
437
+ </p>
438
+ )}
134
439
  </div>
135
- <p className="text-[12px] text-text-3/60 font-mono truncate">
136
- {server.transport === 'stdio' ? server.command : server.url}
137
- </p>
138
- </button>
139
- ))}
140
- </div>
440
+ ))}
441
+ </div>
442
+ </>
141
443
  )}
142
444
  </div>
143
445
  )
@@ -4,6 +4,7 @@ import { useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { BottomSheet } from '@/components/shared/bottom-sheet'
6
6
  import { api } from '@/lib/api-client'
7
+ import { toast } from 'sonner'
7
8
  import type { McpServerConfig, McpTransport } from '@/types'
8
9
 
9
10
  function McpServerForm({ editing, onClose, loadMcpServers }: {
@@ -58,20 +59,31 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
58
59
  } else {
59
60
  data.url = url.trim()
60
61
  }
61
- if (editing) {
62
- await api('PUT', `/mcp-servers/${editing.id}`, data)
63
- } else {
64
- await api('POST', '/mcp-servers', data)
62
+ try {
63
+ if (editing) {
64
+ await api('PUT', `/mcp-servers/${editing.id}`, data)
65
+ toast.success('MCP server updated')
66
+ } else {
67
+ await api('POST', '/mcp-servers', data)
68
+ toast.success('MCP server created')
69
+ }
70
+ await loadMcpServers()
71
+ onClose()
72
+ } catch (err: unknown) {
73
+ toast.error(err instanceof Error ? err.message : 'Failed to save server')
65
74
  }
66
- await loadMcpServers()
67
- onClose()
68
75
  }
69
76
 
70
77
  const handleDelete = async () => {
71
- if (editing) {
78
+ if (!editing) return
79
+ if (!confirm('Delete this MCP server?')) return
80
+ try {
72
81
  await api('DELETE', `/mcp-servers/${editing.id}`)
82
+ toast.success('MCP server deleted')
73
83
  await loadMcpServers()
74
84
  onClose()
85
+ } catch (err: unknown) {
86
+ toast.error(err instanceof Error ? err.message : 'Failed to delete server')
75
87
  }
76
88
  }
77
89
 
@@ -82,8 +94,12 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
82
94
  try {
83
95
  const result = await api<{ ok: boolean; tools?: string[]; error?: string }>('POST', `/mcp-servers/${editing.id}/test`)
84
96
  setTestResult(result)
97
+ if (result.ok) toast.success('Connection test passed')
98
+ else toast.error(result.error || 'Connection test failed')
85
99
  } catch (err: unknown) {
86
- setTestResult({ ok: false, error: err instanceof Error ? err.message : 'Test failed' })
100
+ const msg = err instanceof Error ? err.message : 'Test failed'
101
+ setTestResult({ ok: false, error: msg })
102
+ toast.error(msg)
87
103
  }
88
104
  setTesting(false)
89
105
  }
@@ -99,7 +115,7 @@ function McpServerForm({ editing, onClose, loadMcpServers }: {
99
115
  <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
100
116
  {editing ? 'Edit MCP Server' : 'New MCP Server'}
101
117
  </h2>
102
- <p className="text-[14px] text-text-3">Configure an MCP server to provide tools to agents</p>
118
+ <p className="text-[14px] text-text-3">Configure an MCP server to provide plugins to agents</p>
103
119
  </div>
104
120
 
105
121
  <div className="mb-8">
@@ -80,7 +80,7 @@ export function MemoryList({ inSidebar: _inSidebar, onSelect }: Props) {
80
80
  return (
81
81
  <div className="flex-1 flex flex-col overflow-y-auto">
82
82
  {/* Search */}
83
- <div className="px-3 py-2 shrink-0">
83
+ <div className="px-3 py-2 shrink-0" style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
84
84
  <input
85
85
  type="text"
86
86
  value={search}
@@ -94,7 +94,7 @@ export function MemoryList({ inSidebar: _inSidebar, onSelect }: Props) {
94
94
 
95
95
  {/* Agent filter tabs */}
96
96
  {entries.length > 0 && hasMultipleAgents && (
97
- <div className="px-3 pb-1.5 shrink-0">
97
+ <div className="px-3 pb-1.5 shrink-0" style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.05s both' }}>
98
98
  <div className="flex gap-1 flex-wrap">
99
99
  <button
100
100
  onClick={() => setMemoryAgentFilter(null)}
@@ -125,7 +125,7 @@ export function MemoryList({ inSidebar: _inSidebar, onSelect }: Props) {
125
125
 
126
126
  {/* Category filter */}
127
127
  {entries.length > 0 && uniqueCategories.length > 1 && (
128
- <div className="px-3 pb-1.5 shrink-0">
128
+ <div className="px-3 pb-1.5 shrink-0" style={{ animation: 'fade-up 0.4s var(--ease-spring) 0.1s both' }}>
129
129
  <div className="flex gap-1 flex-wrap">
130
130
  <button
131
131
  onClick={() => setCategoryFilter('')}
@@ -153,21 +153,28 @@ export function MemoryList({ inSidebar: _inSidebar, onSelect }: Props) {
153
153
  {/* Memory cards */}
154
154
  {filtered.length > 0 ? (
155
155
  <div className="flex flex-col gap-0.5 px-2 pb-4">
156
- {filtered.map((e) => (
157
- <MemoryCard
156
+ {filtered.map((e, idx) => (
157
+ <div
158
158
  key={e.id}
159
- entry={e}
160
- active={e.id === selectedMemoryId}
161
- agentName={e.agentId ? (agents[e.agentId]?.name || null) : null}
162
- onClick={() => {
163
- setSelectedMemoryId(e.id)
164
- onSelect?.()
159
+ style={{
160
+ animation: 'fade-up 0.4s var(--ease-spring) both',
161
+ animationDelay: `${0.15 + idx * 0.02}s`
165
162
  }}
166
- />
163
+ >
164
+ <MemoryCard
165
+ entry={e}
166
+ active={e.id === selectedMemoryId}
167
+ agentName={e.agentId ? (agents[e.agentId]?.name || null) : null}
168
+ onClick={() => {
169
+ setSelectedMemoryId(e.id)
170
+ onSelect?.()
171
+ }}
172
+ />
173
+ </div>
167
174
  ))}
168
175
  </div>
169
176
  ) : error ? (
170
- <div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
177
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center" style={{ animation: 'fade-up 0.5s var(--ease-spring)' }}>
171
178
  <p className="font-display text-[14px] font-600 text-text-2">Couldn&apos;t load memories</p>
172
179
  <p className="text-[12px] text-text-3/60">{error}</p>
173
180
  <button