@swarmclawai/swarmclaw 0.7.7 → 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 (281) hide show
  1. package/README.md +12 -14
  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 +23 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +46 -3
  17. package/src/app/api/connectors/route.ts +12 -8
  18. package/src/app/api/external-agents/route.test.ts +165 -0
  19. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  20. package/src/app/api/gateways/[id]/route.ts +2 -0
  21. package/src/app/api/gateways/health-route.test.ts +135 -0
  22. package/src/app/api/gateways/route.ts +2 -0
  23. package/src/app/api/mcp-servers/route.test.ts +130 -0
  24. package/src/app/api/openclaw/deploy/route.ts +38 -5
  25. package/src/app/api/plugins/install/route.ts +46 -6
  26. package/src/app/api/plugins/marketplace/route.ts +48 -15
  27. package/src/app/api/preview-server/route.ts +26 -11
  28. package/src/app/api/projects/[id]/route.ts +6 -2
  29. package/src/app/api/projects/route.ts +4 -3
  30. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  31. package/src/app/api/schedules/route.test.ts +86 -0
  32. package/src/app/api/schedules/route.ts +6 -1
  33. package/src/app/api/secrets/[id]/route.ts +1 -0
  34. package/src/app/api/secrets/route.ts +2 -1
  35. package/src/app/api/settings/route.ts +2 -0
  36. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  37. package/src/app/api/setup/check-provider/route.ts +40 -10
  38. package/src/app/api/skills/[id]/route.ts +12 -0
  39. package/src/app/api/skills/import/route.ts +14 -12
  40. package/src/app/api/skills/route.ts +13 -1
  41. package/src/app/api/tasks/[id]/route.ts +10 -1
  42. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  43. package/src/app/api/tasks/import/github/route.ts +337 -0
  44. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  45. package/src/app/api/wallets/[id]/route.ts +79 -33
  46. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  47. package/src/app/api/wallets/route.ts +78 -61
  48. package/src/app/api/webhooks/[id]/route.ts +33 -6
  49. package/src/app/api/webhooks/route.test.ts +272 -0
  50. package/src/cli/index.js +1 -0
  51. package/src/cli/spec.js +1 -0
  52. package/src/components/agents/agent-card.tsx +9 -2
  53. package/src/components/agents/agent-chat-list.tsx +18 -2
  54. package/src/components/agents/agent-list.tsx +1 -0
  55. package/src/components/agents/agent-sheet.tsx +257 -38
  56. package/src/components/agents/inspector-panel.tsx +41 -0
  57. package/src/components/canvas/canvas-panel.tsx +236 -65
  58. package/src/components/chat/chat-area.tsx +36 -19
  59. package/src/components/chat/chat-card.tsx +36 -13
  60. package/src/components/chat/chat-header.tsx +48 -16
  61. package/src/components/chat/chat-list.tsx +28 -4
  62. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  63. package/src/components/chat/delegation-banner.test.ts +14 -1
  64. package/src/components/chat/delegation-banner.tsx +1 -1
  65. package/src/components/chat/message-bubble.tsx +208 -145
  66. package/src/components/chat/message-list.tsx +48 -19
  67. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  68. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  69. package/src/components/connectors/connector-health.tsx +1 -1
  70. package/src/components/connectors/connector-list.tsx +7 -2
  71. package/src/components/connectors/connector-sheet.tsx +337 -148
  72. package/src/components/gateways/gateway-sheet.tsx +2 -2
  73. package/src/components/layout/app-layout.tsx +40 -23
  74. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  76. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  77. package/src/components/plugins/plugin-list.tsx +45 -9
  78. package/src/components/plugins/plugin-sheet.tsx +55 -7
  79. package/src/components/projects/project-detail.tsx +217 -0
  80. package/src/components/projects/project-sheet.tsx +176 -4
  81. package/src/components/providers/provider-list.tsx +2 -1
  82. package/src/components/providers/provider-sheet.tsx +21 -2
  83. package/src/components/schedules/schedule-card.tsx +25 -1
  84. package/src/components/schedules/schedule-sheet.tsx +44 -2
  85. package/src/components/secrets/secret-sheet.tsx +21 -2
  86. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  87. package/src/components/shared/bottom-sheet.tsx +13 -3
  88. package/src/components/shared/command-palette.tsx +8 -1
  89. package/src/components/shared/confirm-dialog.tsx +19 -4
  90. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  91. package/src/components/shared/connector-platform-icon.tsx +39 -6
  92. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  93. package/src/components/shared/settings/section-capability-policy.tsx +45 -3
  94. package/src/components/shared/settings/section-voice.tsx +11 -3
  95. package/src/components/skills/skill-list.tsx +25 -0
  96. package/src/components/skills/skill-sheet.tsx +84 -12
  97. package/src/components/tasks/approvals-panel.tsx +289 -34
  98. package/src/components/tasks/task-board.tsx +410 -25
  99. package/src/components/tasks/task-card.tsx +66 -8
  100. package/src/components/tasks/task-sheet.tsx +16 -4
  101. package/src/components/ui/dialog.tsx +2 -2
  102. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  103. package/src/components/wallets/wallet-panel.tsx +435 -90
  104. package/src/components/wallets/wallet-section.tsx +198 -48
  105. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  106. package/src/lib/approval-display.ts +20 -0
  107. package/src/lib/canvas-content.ts +198 -0
  108. package/src/lib/chat-artifact-summary.ts +165 -0
  109. package/src/lib/chat-display.test.ts +91 -0
  110. package/src/lib/chat-display.ts +58 -0
  111. package/src/lib/chat-streaming-state.test.ts +47 -1
  112. package/src/lib/chat-streaming-state.ts +42 -0
  113. package/src/lib/ollama-model.ts +10 -0
  114. package/src/lib/openclaw-endpoint.test.ts +8 -0
  115. package/src/lib/openclaw-endpoint.ts +6 -1
  116. package/src/lib/plugin-install-cors.ts +46 -0
  117. package/src/lib/plugin-sources.test.ts +43 -0
  118. package/src/lib/plugin-sources.ts +77 -0
  119. package/src/lib/providers/ollama.ts +16 -6
  120. package/src/lib/providers/openclaw.test.ts +54 -0
  121. package/src/lib/providers/openclaw.ts +127 -11
  122. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  123. package/src/lib/schedule-dedupe.test.ts +66 -1
  124. package/src/lib/schedule-dedupe.ts +169 -12
  125. package/src/lib/schedule-origin.test.ts +20 -0
  126. package/src/lib/schedule-origin.ts +15 -0
  127. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  128. package/src/lib/server/agent-availability.ts +16 -0
  129. package/src/lib/server/agent-runtime-config.ts +12 -4
  130. package/src/lib/server/agent-thread-session.test.ts +51 -0
  131. package/src/lib/server/agent-thread-session.ts +7 -0
  132. package/src/lib/server/approval-match.ts +205 -0
  133. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  134. package/src/lib/server/approvals.ts +214 -1
  135. package/src/lib/server/assistant-control.test.ts +29 -0
  136. package/src/lib/server/assistant-control.ts +23 -0
  137. package/src/lib/server/build-llm.test.ts +79 -0
  138. package/src/lib/server/build-llm.ts +14 -4
  139. package/src/lib/server/canvas-content.test.ts +32 -0
  140. package/src/lib/server/canvas-content.ts +6 -0
  141. package/src/lib/server/capability-router.test.ts +33 -0
  142. package/src/lib/server/capability-router.ts +80 -19
  143. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  144. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  145. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  146. package/src/lib/server/chat-execution.ts +378 -73
  147. package/src/lib/server/clawhub-client.test.ts +14 -8
  148. package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
  149. package/src/lib/server/connectors/manager.test.ts +1147 -0
  150. package/src/lib/server/connectors/manager.ts +461 -137
  151. package/src/lib/server/connectors/pairing.ts +26 -5
  152. package/src/lib/server/connectors/types.ts +2 -0
  153. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  154. package/src/lib/server/connectors/whatsapp.ts +271 -47
  155. package/src/lib/server/context-manager.ts +6 -1
  156. package/src/lib/server/daemon-state.ts +84 -47
  157. package/src/lib/server/data-dir.test.ts +37 -0
  158. package/src/lib/server/data-dir.ts +20 -1
  159. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  160. package/src/lib/server/devserver-launch.test.ts +60 -0
  161. package/src/lib/server/devserver-launch.ts +85 -0
  162. package/src/lib/server/elevenlabs.test.ts +247 -1
  163. package/src/lib/server/elevenlabs.ts +147 -43
  164. package/src/lib/server/ethereum.ts +590 -0
  165. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  166. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  167. package/src/lib/server/eval/agent-regression.ts +383 -11
  168. package/src/lib/server/evm-swap.ts +475 -0
  169. package/src/lib/server/execution-log.ts +1 -0
  170. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  171. package/src/lib/server/heartbeat-service.ts +20 -11
  172. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  173. package/src/lib/server/heartbeat-wake.ts +338 -57
  174. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  175. package/src/lib/server/main-agent-loop.test.ts +260 -0
  176. package/src/lib/server/main-agent-loop.ts +559 -14
  177. package/src/lib/server/mcp-client.test.ts +16 -0
  178. package/src/lib/server/mcp-client.ts +25 -0
  179. package/src/lib/server/memory-integration.test.ts +719 -0
  180. package/src/lib/server/memory-policy.test.ts +43 -0
  181. package/src/lib/server/memory-policy.ts +132 -0
  182. package/src/lib/server/memory-tiers.test.ts +60 -0
  183. package/src/lib/server/memory-tiers.ts +16 -0
  184. package/src/lib/server/ollama-runtime.ts +58 -0
  185. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  186. package/src/lib/server/openclaw-deploy.ts +557 -81
  187. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  188. package/src/lib/server/openclaw-gateway.ts +10 -4
  189. package/src/lib/server/openclaw-health.test.ts +35 -0
  190. package/src/lib/server/openclaw-health.ts +215 -47
  191. package/src/lib/server/orchestrator-lg.ts +3 -2
  192. package/src/lib/server/orchestrator.ts +2 -0
  193. package/src/lib/server/plugins-advanced.test.ts +351 -0
  194. package/src/lib/server/plugins.ts +211 -6
  195. package/src/lib/server/project-context.ts +162 -0
  196. package/src/lib/server/project-utils.ts +150 -0
  197. package/src/lib/server/queue-advanced.test.ts +528 -0
  198. package/src/lib/server/queue-followups.test.ts +409 -2
  199. package/src/lib/server/queue-reconcile.test.ts +128 -0
  200. package/src/lib/server/queue.ts +527 -68
  201. package/src/lib/server/scheduler.ts +29 -1
  202. package/src/lib/server/session-note.test.ts +36 -0
  203. package/src/lib/server/session-note.ts +42 -0
  204. package/src/lib/server/session-run-manager.ts +83 -4
  205. package/src/lib/server/session-tools/canvas.ts +14 -12
  206. package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
  207. package/src/lib/server/session-tools/connector.test.ts +138 -0
  208. package/src/lib/server/session-tools/connector.ts +366 -54
  209. package/src/lib/server/session-tools/context.ts +17 -3
  210. package/src/lib/server/session-tools/crud.ts +484 -84
  211. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  212. package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
  213. package/src/lib/server/session-tools/delegate.ts +102 -10
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  215. package/src/lib/server/session-tools/discovery.ts +80 -12
  216. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  217. package/src/lib/server/session-tools/file.ts +43 -4
  218. package/src/lib/server/session-tools/human-loop.ts +35 -5
  219. package/src/lib/server/session-tools/index.ts +44 -9
  220. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  221. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  222. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  223. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  224. package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
  225. package/src/lib/server/session-tools/memory.test.ts +93 -0
  226. package/src/lib/server/session-tools/memory.ts +554 -75
  227. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  228. package/src/lib/server/session-tools/platform-access.test.ts +58 -0
  229. package/src/lib/server/session-tools/platform.ts +60 -19
  230. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  231. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  232. package/src/lib/server/session-tools/schedule.ts +6 -1
  233. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  234. package/src/lib/server/session-tools/shell.ts +22 -3
  235. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  236. package/src/lib/server/session-tools/wallet.ts +1374 -139
  237. package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
  238. package/src/lib/server/session-tools/web.ts +621 -70
  239. package/src/lib/server/skill-discovery.ts +128 -0
  240. package/src/lib/server/skill-eligibility.test.ts +84 -0
  241. package/src/lib/server/skill-eligibility.ts +95 -0
  242. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  243. package/src/lib/server/skill-prompt-budget.ts +125 -0
  244. package/src/lib/server/skills-normalize.test.ts +54 -0
  245. package/src/lib/server/skills-normalize.ts +372 -26
  246. package/src/lib/server/solana.ts +214 -29
  247. package/src/lib/server/storage.ts +65 -36
  248. package/src/lib/server/stream-agent-chat.test.ts +437 -2
  249. package/src/lib/server/stream-agent-chat.ts +957 -79
  250. package/src/lib/server/system-events.ts +1 -1
  251. package/src/lib/server/tool-aliases.ts +2 -0
  252. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  253. package/src/lib/server/tool-capability-policy.test.ts +24 -0
  254. package/src/lib/server/tool-capability-policy.ts +29 -1
  255. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  256. package/src/lib/server/tool-loop-detection.ts +260 -0
  257. package/src/lib/server/tool-planning.test.ts +44 -0
  258. package/src/lib/server/tool-planning.ts +271 -0
  259. package/src/lib/server/wallet-execution.test.ts +198 -0
  260. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  261. package/src/lib/server/wallet-portfolio.ts +724 -0
  262. package/src/lib/server/wallet-service.test.ts +57 -0
  263. package/src/lib/server/wallet-service.ts +213 -0
  264. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  265. package/src/lib/server/watch-jobs.ts +17 -2
  266. package/src/lib/server/workspace-context.ts +111 -0
  267. package/src/lib/skill-save-payload.test.ts +39 -0
  268. package/src/lib/skill-save-payload.ts +37 -0
  269. package/src/lib/tasks.ts +28 -0
  270. package/src/lib/tool-definitions.ts +2 -1
  271. package/src/lib/tool-event-summary.test.ts +30 -0
  272. package/src/lib/tool-event-summary.ts +37 -0
  273. package/src/lib/validation/schemas.ts +1 -0
  274. package/src/lib/wallet-transactions.test.ts +75 -0
  275. package/src/lib/wallet-transactions.ts +43 -0
  276. package/src/lib/wallet.test.ts +17 -0
  277. package/src/lib/wallet.ts +183 -0
  278. package/src/proxy.test.ts +31 -0
  279. package/src/proxy.ts +34 -2
  280. package/src/stores/use-chat-store.ts +15 -1
  281. package/src/types/index.ts +249 -14
@@ -1,74 +1,91 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { genId } from '@/lib/id'
3
- import { loadWallets, upsertWallet, loadAgents, saveAgents } from '@/lib/server/storage'
4
- import { generateSolanaKeypair } from '@/lib/server/solana'
5
- import { notify } from '@/lib/server/ws-hub'
6
- import type { AgentWallet, WalletChain } from '@/types'
2
+ import { loadAgents, loadWallets } from '@/lib/server/storage'
3
+ import { createAgentWallet, getAgentActiveWalletId, getWalletPortfolioSnapshot, stripWalletPrivateKey } from '@/lib/server/wallet-service'
4
+ import { buildEmptyWalletPortfolio } from '@/lib/server/wallet-portfolio'
5
+ import type { AgentWallet, WalletPortfolioSummary } from '@/types'
7
6
  export const dynamic = 'force-dynamic'
7
+ const WALLET_LIST_PORTFOLIO_TIMEOUT_MS = 1500
8
8
 
9
- /** Strip encryptedPrivateKey from wallet for safe API responses */
10
- function stripPrivateKey(wallet: Record<string, unknown>): Record<string, unknown> {
11
- return Object.fromEntries(Object.entries(wallet).filter(([k]) => k !== 'encryptedPrivateKey'))
9
+ function withPortfolio(
10
+ wallet: AgentWallet,
11
+ portfolio: {
12
+ balanceAtomic: string
13
+ balanceFormatted: string
14
+ balanceSymbol: string
15
+ balanceDisplay: string
16
+ balanceLamports?: number
17
+ balanceSol?: number
18
+ assets: unknown[]
19
+ summary: WalletPortfolioSummary
20
+ },
21
+ isActive: boolean,
22
+ ) {
23
+ return {
24
+ ...stripWalletPrivateKey(wallet as unknown as Record<string, unknown>),
25
+ balanceAtomic: portfolio.balanceAtomic,
26
+ balanceFormatted: portfolio.balanceFormatted,
27
+ balanceSymbol: portfolio.balanceSymbol,
28
+ balanceDisplay: portfolio.balanceDisplay,
29
+ balanceLamports: portfolio.balanceLamports,
30
+ balanceSol: portfolio.balanceSol,
31
+ assets: portfolio.assets,
32
+ portfolioSummary: portfolio.summary,
33
+ isActive,
34
+ }
12
35
  }
13
36
 
14
- export async function GET() {
37
+ export async function GET(req: Request) {
15
38
  const wallets = loadWallets() as Record<string, AgentWallet>
16
- const safe = Object.fromEntries(
17
- Object.entries(wallets).map(([id, w]) => [id, stripPrivateKey(w as unknown as Record<string, unknown>)]),
39
+ const agents = loadAgents()
40
+ const { searchParams } = new URL(req.url)
41
+ const agentId = searchParams.get('agentId')?.trim() || ''
42
+ const walletEntries = Object.entries(wallets)
43
+ .filter(([, wallet]) => !agentId || wallet.agentId === agentId)
44
+ const entries = await Promise.all(
45
+ walletEntries.map(async ([id, wallet]) => {
46
+ let portfolio = buildEmptyWalletPortfolio(wallet)
47
+ try {
48
+ portfolio = await getWalletPortfolioSnapshot(wallet, {
49
+ timeoutMs: WALLET_LIST_PORTFOLIO_TIMEOUT_MS,
50
+ allowStale: true,
51
+ })
52
+ } catch {
53
+ // Slow or failed RPC discovery — return empty/stale portfolio for list view
54
+ }
55
+ const activeWalletId = getAgentActiveWalletId(agents[wallet.agentId])
56
+ return [id, withPortfolio(wallet, portfolio, activeWalletId === wallet.id)] as const
57
+ }),
18
58
  )
19
- return NextResponse.json(safe)
59
+ return NextResponse.json(Object.fromEntries(entries))
20
60
  }
21
61
 
22
62
  export async function POST(req: Request) {
23
63
  const body = await req.json()
24
- const agentId = typeof body.agentId === 'string' ? body.agentId.trim() : ''
25
- if (!agentId) {
26
- return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
27
- }
28
-
29
- const agents = loadAgents()
30
- if (!agents[agentId]) {
31
- return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
32
- }
33
-
34
- // Check agent doesn't already have a wallet
35
- const existing = loadWallets() as Record<string, AgentWallet>
36
- const hasWallet = Object.values(existing).some((w) => w.agentId === agentId)
37
- if (hasWallet) {
38
- return NextResponse.json({ error: 'Agent already has a wallet' }, { status: 409 })
64
+ try {
65
+ const wallet = createAgentWallet({
66
+ agentId: body.agentId,
67
+ chain: body.chain,
68
+ provider: body.provider,
69
+ label: body.label,
70
+ requireApproval: body.requireApproval,
71
+ spendingLimitAtomic: body.spendingLimitAtomic ?? body.spendingLimitLamports,
72
+ dailyLimitAtomic: body.dailyLimitAtomic ?? body.dailyLimitLamports,
73
+ })
74
+ return NextResponse.json(stripWalletPrivateKey(wallet as unknown as Record<string, unknown>))
75
+ } catch (err: unknown) {
76
+ const message = err instanceof Error ? err.message : String(err)
77
+ if (message === 'agentId is required') {
78
+ return NextResponse.json({ error: message }, { status: 400 })
79
+ }
80
+ if (/^Unsupported wallet chain or provider: /.test(message)) {
81
+ return NextResponse.json({ error: message }, { status: 400 })
82
+ }
83
+ if (message === 'Agent not found') {
84
+ return NextResponse.json({ error: message }, { status: 404 })
85
+ }
86
+ if (/^Agent already has a (solana|ethereum) wallet$/.test(message)) {
87
+ return NextResponse.json({ error: message }, { status: 409 })
88
+ }
89
+ return NextResponse.json({ error: message }, { status: 500 })
39
90
  }
40
-
41
- const chain: WalletChain = body.chain === 'solana' ? 'solana' : 'solana' // extensible later
42
- const { publicKey, encryptedPrivateKey } = generateSolanaKeypair()
43
-
44
- const id = genId()
45
- const now = Date.now()
46
-
47
- const wallet: AgentWallet = {
48
- id,
49
- agentId,
50
- chain,
51
- publicKey,
52
- encryptedPrivateKey,
53
- label: typeof body.label === 'string' ? body.label : undefined,
54
- spendingLimitLamports: typeof body.spendingLimitLamports === 'number' ? body.spendingLimitLamports : 100_000_000,
55
- dailyLimitLamports: typeof body.dailyLimitLamports === 'number' ? body.dailyLimitLamports : 1_000_000_000,
56
- requireApproval: body.requireApproval !== false,
57
- createdAt: now,
58
- updatedAt: now,
59
- }
60
-
61
- upsertWallet(id, wallet)
62
-
63
- // Link wallet to agent
64
- const agent = agents[agentId]
65
- agent.walletId = id
66
- agent.updatedAt = now
67
- agents[agentId] = agent
68
- saveAgents(agents)
69
-
70
- notify('wallets')
71
- notify('agents')
72
-
73
- return NextResponse.json(stripPrivateKey(wallet as unknown as Record<string, unknown>))
74
91
  }
@@ -13,6 +13,18 @@ import { triggerWebhookWatchJobs } from '@/lib/server/watch-jobs'
13
13
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
14
  const ops: CollectionOps<any> = { load: loadWebhooks, save: saveWebhooks }
15
15
 
16
+ type WebhookPostDeps = {
17
+ enqueueRun: typeof enqueueSessionRun
18
+ enqueueEvent: typeof enqueueSystemEvent
19
+ requestHeartbeat: typeof requestHeartbeatNow
20
+ }
21
+
22
+ const defaultWebhookPostDeps: WebhookPostDeps = {
23
+ enqueueRun: enqueueSessionRun,
24
+ enqueueEvent: enqueueSystemEvent,
25
+ requestHeartbeat: requestHeartbeatNow,
26
+ }
27
+
16
28
  function normalizeEvents(value: unknown): string[] {
17
29
  if (!Array.isArray(value)) return []
18
30
  return value
@@ -58,8 +70,11 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
58
70
  return NextResponse.json({ ok: true })
59
71
  }
60
72
 
61
- export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
62
- const { id } = await params
73
+ export async function handleWebhookPost(
74
+ req: Request,
75
+ id: string,
76
+ deps: WebhookPostDeps = defaultWebhookPostDeps,
77
+ ) {
63
78
  const webhooks = loadWebhooks()
64
79
  const webhook = webhooks[id]
65
80
  if (!webhook) return notFound('Webhook not found')
@@ -176,7 +191,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
176
191
  agentId: agent.id,
177
192
  parentSessionId: null,
178
193
  tools: agent.tools || [],
179
- heartbeatEnabled: agent.heartbeatEnabled ?? true,
194
+ heartbeatEnabled: agent.heartbeatEnabled ?? false,
180
195
  heartbeatIntervalSec: agent.heartbeatIntervalSec ?? null,
181
196
  }
182
197
  sessions[session.id as string] = session
@@ -200,7 +215,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
200
215
  ].join('\n')
201
216
 
202
217
  try {
203
- const run = enqueueSessionRun({
218
+ const run = deps.enqueueRun({
204
219
  sessionId: sid,
205
220
  message: prompt,
206
221
  source: 'webhook',
@@ -209,9 +224,16 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
209
224
  })
210
225
 
211
226
  // Enqueue system event + heartbeat wake
212
- enqueueSystemEvent(sid, `Webhook received: ${webhook.name || id} (${incomingEvent})`)
227
+ deps.enqueueEvent(sid, `Webhook received: ${webhook.name || id} (${incomingEvent})`)
213
228
  if (webhook.agentId) {
214
- requestHeartbeatNow({ agentId: webhook.agentId, reason: 'webhook' })
229
+ deps.requestHeartbeat({
230
+ agentId: webhook.agentId,
231
+ eventId: `webhook:${id}:${incomingEvent}:${Date.now()}`,
232
+ reason: 'webhook',
233
+ source: `webhook:${id}`,
234
+ resumeMessage: `Webhook received: ${webhook.name || id} (${incomingEvent})`,
235
+ detail: payloadPreview || '(empty payload)',
236
+ })
215
237
  }
216
238
 
217
239
  appendWebhookLog(genId(8), {
@@ -262,3 +284,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
262
284
  })
263
285
  }
264
286
  }
287
+
288
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
289
+ const { id } = await params
290
+ return handleWebhookPost(req, id)
291
+ }
@@ -0,0 +1,272 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { afterEach } from 'node:test'
3
+
4
+ import { GET as getWebhookHistory } from './[id]/history/route'
5
+ import { handleWebhookPost } from './[id]/route'
6
+ import {
7
+ loadAgents,
8
+ loadSessions,
9
+ loadWebhookLogs,
10
+ loadWebhookRetryQueue,
11
+ loadWebhooks,
12
+ saveAgents,
13
+ saveSessions,
14
+ saveWebhookLogs,
15
+ saveWebhookRetryQueue,
16
+ saveWebhooks,
17
+ } from '@/lib/server/storage'
18
+
19
+ const originalAgents = loadAgents()
20
+ const originalSessions = loadSessions()
21
+ const originalWebhooks = loadWebhooks()
22
+ const originalWebhookLogs = loadWebhookLogs()
23
+ const originalWebhookRetryQueue = loadWebhookRetryQueue()
24
+
25
+ afterEach(() => {
26
+ saveAgents(originalAgents)
27
+ saveSessions(originalSessions)
28
+ saveWebhooks(originalWebhooks)
29
+ saveWebhookLogs(originalWebhookLogs)
30
+ saveWebhookRetryQueue(originalWebhookRetryQueue)
31
+ })
32
+
33
+ function seedAgent(agentId: string) {
34
+ const agents = loadAgents()
35
+ agents[agentId] = {
36
+ id: agentId,
37
+ name: 'Webhook Agent',
38
+ description: 'Test agent for webhook delivery',
39
+ systemPrompt: 'Handle inbound webhooks.',
40
+ provider: 'openai',
41
+ model: 'gpt-4o-mini',
42
+ credentialId: null,
43
+ apiEndpoint: null,
44
+ tools: ['manage_webhooks'],
45
+ createdAt: 1,
46
+ updatedAt: 1,
47
+ }
48
+ saveAgents(agents)
49
+ }
50
+
51
+ function seedWebhook(webhookId: string, overrides: Record<string, unknown> = {}) {
52
+ const webhooks = loadWebhooks()
53
+ webhooks[webhookId] = {
54
+ id: webhookId,
55
+ name: 'Webhook Smoke',
56
+ source: 'custom',
57
+ events: ['build.completed'],
58
+ agentId: 'agent-webhook-smoke',
59
+ secret: 'secret-smoke',
60
+ isEnabled: true,
61
+ createdAt: 1,
62
+ updatedAt: 1,
63
+ ...overrides,
64
+ }
65
+ saveWebhooks(webhooks)
66
+ }
67
+
68
+ test('handleWebhookPost creates a session, records success history, and triggers follow-up wiring', async () => {
69
+ const webhookId = 'wh-success-smoke'
70
+ seedAgent('agent-webhook-smoke')
71
+ seedWebhook(webhookId)
72
+
73
+ const calls = {
74
+ runs: [] as Array<Record<string, unknown>>,
75
+ events: [] as Array<[string, string]>,
76
+ heartbeats: [] as Array<Record<string, unknown>>,
77
+ }
78
+
79
+ const response = await handleWebhookPost(
80
+ new Request(`http://local/api/webhooks/${webhookId}?event=build.completed`, {
81
+ method: 'POST',
82
+ headers: {
83
+ 'content-type': 'application/json',
84
+ 'x-webhook-secret': 'secret-smoke',
85
+ },
86
+ body: JSON.stringify({ event: 'build.completed', payload: { ok: true } }),
87
+ }),
88
+ webhookId,
89
+ {
90
+ enqueueRun(input) {
91
+ calls.runs.push(input as Record<string, unknown>)
92
+ return {
93
+ runId: 'run-success-smoke',
94
+ position: 0,
95
+ promise: Promise.resolve({} as never),
96
+ abort: () => {},
97
+ }
98
+ },
99
+ enqueueEvent(sessionId, text) {
100
+ calls.events.push([sessionId, text])
101
+ },
102
+ requestHeartbeat(opts) {
103
+ calls.heartbeats.push(opts as Record<string, unknown>)
104
+ },
105
+ },
106
+ )
107
+
108
+ assert.equal(response.status, 200)
109
+ const payload = await response.json() as Record<string, unknown>
110
+ assert.equal(payload.ok, true)
111
+ assert.equal(payload.event, 'build.completed')
112
+ assert.equal(payload.runId, 'run-success-smoke')
113
+
114
+ const sessionId = String(payload.sessionId)
115
+ const session = loadSessions()[sessionId]
116
+ assert.ok(session)
117
+ assert.equal(session.name, `webhook:${webhookId}`)
118
+ assert.equal(session.agentId, 'agent-webhook-smoke')
119
+
120
+ assert.equal(calls.runs.length, 1)
121
+ assert.equal(calls.runs[0].sessionId, sessionId)
122
+ assert.equal(calls.runs[0].source, 'webhook')
123
+ assert.equal(calls.runs[0].mode, 'followup')
124
+ assert.match(String(calls.runs[0].message), /Webhook event received\./)
125
+ assert.match(String(calls.runs[0].message), /Event: build\.completed/)
126
+
127
+ assert.deepEqual(calls.events, [[sessionId, 'Webhook received: Webhook Smoke (build.completed)']])
128
+ assert.deepEqual(calls.heartbeats, [{ agentId: 'agent-webhook-smoke', reason: 'webhook' }])
129
+
130
+ const logEntries = Object.values(loadWebhookLogs()) as Array<Record<string, unknown>>
131
+ const successEntry = logEntries.find((entry) => entry.webhookId === webhookId && entry.status === 'success')
132
+ assert.ok(successEntry)
133
+ assert.equal(successEntry?.sessionId, sessionId)
134
+ assert.equal(successEntry?.runId, 'run-success-smoke')
135
+
136
+ const historyResponse = await getWebhookHistory(new Request(`http://local/api/webhooks/${webhookId}/history`), {
137
+ params: Promise.resolve({ id: webhookId }),
138
+ })
139
+ assert.equal(historyResponse.status, 200)
140
+ const history = await historyResponse.json() as Array<Record<string, unknown>>
141
+ assert.equal(history[0]?.status, 'success')
142
+ assert.equal(history[0]?.webhookId, webhookId)
143
+ })
144
+
145
+ test('handleWebhookPost ignores filtered events without dispatching or logging delivery', async () => {
146
+ const webhookId = 'wh-ignored-smoke'
147
+ seedAgent('agent-webhook-smoke')
148
+ seedWebhook(webhookId, { events: ['build.completed'] })
149
+
150
+ let runCalls = 0
151
+ const response = await handleWebhookPost(
152
+ new Request(`http://local/api/webhooks/${webhookId}`, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'content-type': 'application/json',
156
+ 'x-webhook-secret': 'secret-smoke',
157
+ },
158
+ body: JSON.stringify({ event: 'build.started' }),
159
+ }),
160
+ webhookId,
161
+ {
162
+ enqueueRun() {
163
+ runCalls += 1
164
+ return {
165
+ runId: 'should-not-run',
166
+ position: 0,
167
+ promise: Promise.resolve({} as never),
168
+ abort: () => {},
169
+ }
170
+ },
171
+ enqueueEvent() {},
172
+ requestHeartbeat() {},
173
+ },
174
+ )
175
+
176
+ assert.equal(response.status, 200)
177
+ const payload = await response.json() as Record<string, unknown>
178
+ assert.equal(payload.ignored, true)
179
+ assert.equal(payload.event, 'build.started')
180
+ assert.equal(runCalls, 0)
181
+ assert.equal(Object.values(loadSessions()).some((session: any) => session?.name === `webhook:${webhookId}`), false)
182
+ assert.equal(Object.values(loadWebhookLogs()).some((entry: any) => entry?.webhookId === webhookId), false)
183
+ })
184
+
185
+ test('handleWebhookPost rejects disabled webhooks and invalid secrets with error history', async () => {
186
+ const disabledId = 'wh-disabled-smoke'
187
+ seedWebhook(disabledId, { isEnabled: false, secret: '' })
188
+
189
+ const disabledResponse = await handleWebhookPost(
190
+ new Request(`http://local/api/webhooks/${disabledId}`, { method: 'POST' }),
191
+ disabledId,
192
+ {
193
+ enqueueRun() {
194
+ throw new Error('should not dispatch')
195
+ },
196
+ enqueueEvent() {},
197
+ requestHeartbeat() {},
198
+ },
199
+ )
200
+ assert.equal(disabledResponse.status, 409)
201
+
202
+ const invalidSecretId = 'wh-secret-smoke'
203
+ seedAgent('agent-webhook-smoke')
204
+ seedWebhook(invalidSecretId, { secret: 'top-secret' })
205
+
206
+ const invalidSecretResponse = await handleWebhookPost(
207
+ new Request(`http://local/api/webhooks/${invalidSecretId}`, {
208
+ method: 'POST',
209
+ headers: { 'x-webhook-secret': 'wrong-secret' },
210
+ }),
211
+ invalidSecretId,
212
+ {
213
+ enqueueRun() {
214
+ throw new Error('should not dispatch')
215
+ },
216
+ enqueueEvent() {},
217
+ requestHeartbeat() {},
218
+ },
219
+ )
220
+ assert.equal(invalidSecretResponse.status, 401)
221
+
222
+ const errors = Object.values(loadWebhookLogs()) as Array<Record<string, unknown>>
223
+ const disabledEntry = errors.find((entry) => entry.webhookId === disabledId)
224
+ const invalidSecretEntry = errors.find((entry) => entry.webhookId === invalidSecretId)
225
+ assert.equal(disabledEntry?.error, 'Webhook is disabled')
226
+ assert.equal(invalidSecretEntry?.error, 'Invalid webhook secret')
227
+ })
228
+
229
+ test('handleWebhookPost queues retries when run dispatch throws', async () => {
230
+ const webhookId = 'wh-retry-smoke'
231
+ seedAgent('agent-webhook-smoke')
232
+ seedWebhook(webhookId)
233
+
234
+ const heartbeats: Array<Record<string, unknown>> = []
235
+ const response = await handleWebhookPost(
236
+ new Request(`http://local/api/webhooks/${webhookId}`, {
237
+ method: 'POST',
238
+ headers: {
239
+ 'content-type': 'application/json',
240
+ 'x-webhook-secret': 'secret-smoke',
241
+ },
242
+ body: JSON.stringify({ event: 'build.completed', payload: { ok: false } }),
243
+ }),
244
+ webhookId,
245
+ {
246
+ enqueueRun() {
247
+ throw new Error('dispatch exploded')
248
+ },
249
+ enqueueEvent() {},
250
+ requestHeartbeat(opts) {
251
+ heartbeats.push(opts as Record<string, unknown>)
252
+ },
253
+ },
254
+ )
255
+
256
+ assert.equal(response.status, 200)
257
+ const payload = await response.json() as Record<string, unknown>
258
+ assert.equal(payload.retryQueued, true)
259
+ assert.equal(payload.error, 'dispatch exploded')
260
+ assert.equal(heartbeats.length, 0)
261
+
262
+ const retries = Object.values(loadWebhookRetryQueue()) as Array<Record<string, unknown>>
263
+ const retryEntry = retries.find((entry) => entry.webhookId === webhookId)
264
+ assert.ok(retryEntry)
265
+ assert.equal(retryEntry?.attempts, 1)
266
+ assert.equal(retryEntry?.deadLettered, false)
267
+
268
+ const errorLogs = Object.values(loadWebhookLogs()) as Array<Record<string, unknown>>
269
+ const retryLog = errorLogs.find((entry) => entry.webhookId === webhookId)
270
+ assert.ok(retryLog)
271
+ assert.match(String(retryLog?.error), /Dispatch failed, queued for retry: dispatch exploded/)
272
+ })
package/src/cli/index.js CHANGED
@@ -586,6 +586,7 @@ const COMMAND_GROUPS = [
586
586
  cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'),
587
587
  cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }),
588
588
  cmd('approve', 'POST', '/tasks/:id/approve', 'Approve or reject a pending tool execution', { expectsJsonBody: true }),
589
+ cmd('import-github', 'POST', '/tasks/import/github', 'Import GitHub issues into tasks', { expectsJsonBody: true }),
589
590
  cmd('metrics', 'GET', '/tasks/metrics', 'Get task board metrics (supports --query range=24h|7d|30d)'),
590
591
  ],
591
592
  },
package/src/cli/spec.js CHANGED
@@ -447,6 +447,7 @@ const COMMAND_GROUPS = {
447
447
  delete: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
448
448
  archive: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
449
449
  approve: { description: 'Approve or reject a pending tool execution', method: 'POST', path: '/tasks/:id/approve', params: ['id'] },
450
+ 'import-github': { description: 'Import GitHub issues into tasks', method: 'POST', path: '/tasks/import/github' },
450
451
  metrics: { description: 'Get task board metrics (supports --query range=24h|7d|30d)', method: 'GET', path: '/tasks/metrics' },
451
452
  },
452
453
  },
@@ -63,6 +63,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
63
63
  },
64
64
  ].filter((entry) => entry.budget !== null)
65
65
  const canDelegateToAgents = agent.platformAssignScope === 'all'
66
+ const agentDisabled = agent.disabled === true
66
67
  useWs(`heartbeat:agent:${agent.id}`, () => {
67
68
  setHeartbeatPulse(true)
68
69
  setTimeout(() => setHeartbeatPulse(false), 1500)
@@ -125,6 +126,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
125
126
  onClick={handleClick}
126
127
  className={`group relative py-3.5 px-4 cursor-pointer rounded-[14px]
127
128
  transition-all duration-200 active:scale-[0.98]
129
+ ${agentDisabled ? 'opacity-70' : ''}
128
130
  ${isSelected
129
131
  ? 'bg-white/[0.04] border border-white/[0.08]'
130
132
  : 'bg-transparent border border-transparent hover:bg-white/[0.05] hover:border-white/[0.08]'}`}
@@ -197,6 +199,11 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
197
199
  {pendingApprovalCount} {pendingApprovalCount === 1 ? 'approval' : 'approvals'}
198
200
  </span>
199
201
  )}
202
+ {agentDisabled && (
203
+ <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-amber-300 bg-amber-400/[0.08] border border-amber-400/15 px-2 py-0.5 rounded-[6px]">
204
+ disabled
205
+ </span>
206
+ )}
200
207
  {isDefault && (
201
208
  <span className="shrink-0 text-[10px] font-600 uppercase tracking-wider text-accent-bright bg-accent-soft px-2 py-0.5 rounded-[6px]">
202
209
  default
@@ -205,12 +212,12 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
205
212
  {canDelegateToAgents && (
206
213
  <button
207
214
  onClick={handleRunClick}
208
- disabled={running}
215
+ disabled={running || agentDisabled}
209
216
  className="shrink-0 text-[10px] font-600 uppercase tracking-wider px-2.5 py-1 rounded-[6px] cursor-pointer
210
217
  transition-all border-none bg-accent-bright/20 text-accent-bright hover:bg-accent-bright/30 disabled:opacity-40"
211
218
  style={{ fontFamily: 'inherit' }}
212
219
  >
213
- {running ? '...' : 'Run'}
220
+ {agentDisabled ? 'Off' : running ? '...' : 'Run'}
214
221
  </button>
215
222
  )}
216
223
  {canDelegateToAgents && (
@@ -167,6 +167,10 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
167
167
  }, [filteredAgents.map((a) => a.id).join(',')])
168
168
 
169
169
  const handleSelect = async (agent: Agent) => {
170
+ if (agent.disabled === true && !agent.threadSessionId) {
171
+ toast.error(`${agent.name} is disabled. Re-enable it to start a new chat.`)
172
+ return
173
+ }
170
174
  await setCurrentAgent(agent.id)
171
175
  // Load messages for the thread
172
176
  const state = useAppStore.getState()
@@ -274,7 +278,8 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
274
278
  const lastMsg = threadSession?.messages?.at(-1)
275
279
  const heartbeatOn = defaultAgent.heartbeatEnabled === true && (defaultAgent.plugins?.length ?? 0) > 0
276
280
  const recentlyActive = (threadSession?.lastActiveAt ?? 0) > Date.now() - 30 * 60 * 1000
277
- const isWorking = runningAgentIds.has(defaultAgent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(defaultAgent.id)
281
+ const isDisabled = defaultAgent.disabled === true
282
+ const isWorking = !isDisabled && (runningAgentIds.has(defaultAgent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(defaultAgent.id))
278
283
  const isTyping = streamingSessionId === defaultAgent.threadSessionId
279
284
  const preview = lastMsg?.text?.slice(0, 100)?.replace(/\n/g, ' ') || 'Your primary shortcut chat.'
280
285
  const isActive = currentAgentId === defaultAgent.id
@@ -313,6 +318,11 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
313
318
  <span className="font-display text-[14px] font-700 truncate text-text tracking-[-0.01em]">
314
319
  {defaultAgent.name}
315
320
  </span>
321
+ {isDisabled && (
322
+ <span className="px-1.5 py-0.5 rounded-[6px] bg-amber-400/[0.08] text-amber-300 text-[9px] font-700 uppercase tracking-[0.08em]">
323
+ Disabled
324
+ </span>
325
+ )}
316
326
  <span className="px-1.5 py-0.5 rounded-[6px] bg-accent-bright/12 text-accent-bright text-[9px] font-700 uppercase tracking-[0.08em]">
317
327
  Shortcut
318
328
  </span>
@@ -359,7 +369,8 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
359
369
  const isActive = currentAgentId === agent.id
360
370
  const heartbeatOn = agent.heartbeatEnabled === true && (agent.plugins?.length ?? 0) > 0
361
371
  const recentlyActive = (threadSession?.lastActiveAt ?? 0) > Date.now() - 30 * 60 * 1000
362
- const isWorking = runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(agent.id)
372
+ const isDisabled = agent.disabled === true
373
+ const isWorking = !isDisabled && (runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(agent.id))
363
374
  const isTyping = streamingSessionId === agent.threadSessionId
364
375
  const preview = lastMsg?.text?.slice(0, 80)?.replace(/\n/g, ' ') || ''
365
376
 
@@ -395,6 +406,11 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
395
406
  <span className="font-display text-[13.5px] font-600 truncate flex-1 tracking-[-0.01em]">
396
407
  {agent.name}
397
408
  </span>
409
+ {isDisabled && (
410
+ <span className="px-1.5 py-0.5 rounded-[6px] bg-amber-400/[0.08] text-amber-300 text-[9px] font-700 uppercase tracking-[0.08em] shrink-0">
411
+ Disabled
412
+ </span>
413
+ )}
398
414
  {appSettings.defaultAgentId === agent.id && (
399
415
  <span className="px-1.5 py-0.5 rounded-[6px] bg-accent-bright/10 text-accent-bright text-[9px] font-700 uppercase tracking-[0.08em] shrink-0">
400
416
  Default
@@ -70,6 +70,7 @@ export function AgentList({ inSidebar }: Props) {
70
70
  const ids = new Set<string>()
71
71
  const recentThreshold = now - 30 * 60 * 1000
72
72
  for (const a of Object.values(agents)) {
73
+ if (a.disabled === true) continue
73
74
  if (a.heartbeatEnabled === true && (a.plugins?.length ?? 0) > 0) { ids.add(a.id); continue }
74
75
  // Check if any session for this agent was active in the last 30 minutes
75
76
  for (const s of Object.values(sessions)) {