@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.
- package/README.md +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -1,74 +1,91 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
|
17
|
-
|
|
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(
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
62
|
-
|
|
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 ??
|
|
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 =
|
|
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
|
-
|
|
227
|
+
deps.enqueueEvent(sid, `Webhook received: ${webhook.name || id} (${incomingEvent})`)
|
|
213
228
|
if (webhook.agentId) {
|
|
214
|
-
|
|
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
|
|
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
|
|
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)) {
|