@swarmclawai/swarmclaw 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -15
- 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 +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- 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/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/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 +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- 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/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/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 +7 -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 +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- 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 +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- 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 +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- 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 +1 -1
- 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 +189 -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 +15 -10
- 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/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 +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- 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 +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- 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/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- 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 +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- 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 +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- 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.ts +4 -2
- 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-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 +210 -14
|
@@ -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)) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useRef, useState } from 'react'
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { createAgent, updateAgent, deleteAgent } from '@/lib/agents'
|
|
6
6
|
import { api } from '@/lib/api-client'
|
|
@@ -18,11 +18,19 @@ import { copyTextToClipboard } from '@/lib/clipboard'
|
|
|
18
18
|
import { SectionLabel } from '@/components/shared/section-label'
|
|
19
19
|
import { SoulLibraryPicker } from './soul-library-picker'
|
|
20
20
|
import { HintTip } from '@/components/shared/hint-tip'
|
|
21
|
+
import { isOllamaCloudModel } from '@/lib/ollama-model'
|
|
21
22
|
|
|
22
23
|
const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
23
24
|
const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
|
|
24
25
|
|
|
25
26
|
type AgentSheetSectionId = 'overview' | 'instructions' | 'model' | 'tools'
|
|
27
|
+
type SafeAgentWallet = Omit<AgentWallet, 'encryptedPrivateKey'> & {
|
|
28
|
+
balanceAtomic?: string
|
|
29
|
+
balanceLamports?: number
|
|
30
|
+
balanceFormatted?: string
|
|
31
|
+
balanceSymbol?: string
|
|
32
|
+
isActive?: boolean
|
|
33
|
+
}
|
|
26
34
|
|
|
27
35
|
function SectionCard({
|
|
28
36
|
title,
|
|
@@ -190,6 +198,7 @@ export function AgentSheet() {
|
|
|
190
198
|
const [memoryScopeMode, setMemoryScopeMode] = useState<'auto' | 'all' | 'global' | 'agent' | 'session' | 'project'>('auto')
|
|
191
199
|
const [memoryTierPreference, setMemoryTierPreference] = useState<'working' | 'durable' | 'archive' | 'blended'>('blended')
|
|
192
200
|
const [autoRecovery, setAutoRecovery] = useState(false)
|
|
201
|
+
const [disabled, setDisabled] = useState(false)
|
|
193
202
|
const [voiceId, setVoiceId] = useState('')
|
|
194
203
|
const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
|
|
195
204
|
const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
|
|
@@ -211,7 +220,7 @@ export function AgentSheet() {
|
|
|
211
220
|
const [dailyBudget, setDailyBudget] = useState('')
|
|
212
221
|
const [monthlyBudget, setMonthlyBudget] = useState('')
|
|
213
222
|
const [budgetAction, setBudgetAction] = useState<'warn' | 'block'>('warn')
|
|
214
|
-
const [
|
|
223
|
+
const [agentWallets, setAgentWallets] = useState<SafeAgentWallet[]>([])
|
|
215
224
|
const [addingKey, setAddingKey] = useState(false)
|
|
216
225
|
const [newKeyName, setNewKeyName] = useState('')
|
|
217
226
|
const [newKeyValue, setNewKeyValue] = useState('')
|
|
@@ -245,6 +254,21 @@ export function AgentSheet() {
|
|
|
245
254
|
e.target.value = ''
|
|
246
255
|
}
|
|
247
256
|
|
|
257
|
+
const loadAgentWallets = useCallback(async (agentId: string) => {
|
|
258
|
+
try {
|
|
259
|
+
const wallets = await api<Record<string, SafeAgentWallet>>('GET', `/wallets?agentId=${encodeURIComponent(agentId)}`)
|
|
260
|
+
const matches = Object.values(wallets)
|
|
261
|
+
.filter((wallet) => wallet.agentId === agentId)
|
|
262
|
+
.sort((a, b) => {
|
|
263
|
+
if ((a.isActive ? 1 : 0) !== (b.isActive ? 1 : 0)) return a.isActive ? -1 : 1
|
|
264
|
+
return a.chain.localeCompare(b.chain)
|
|
265
|
+
})
|
|
266
|
+
setAgentWallets(matches)
|
|
267
|
+
} catch {
|
|
268
|
+
setAgentWallets([])
|
|
269
|
+
}
|
|
270
|
+
}, [])
|
|
271
|
+
|
|
248
272
|
const currentProvider = providers.find((p) => p.id === provider)
|
|
249
273
|
const providerCredentials = Object.values(credentials).filter((c) => c.provider === provider)
|
|
250
274
|
const openclawCredentials = Object.values(credentials).filter((c) => c.provider === 'openclaw')
|
|
@@ -307,7 +331,11 @@ export function AgentSheet() {
|
|
|
307
331
|
setFallbackCredentialIds(editing.fallbackCredentialIds || [])
|
|
308
332
|
setCapabilities(Array.isArray(editing.capabilities) ? editing.capabilities : [])
|
|
309
333
|
setCapInput('')
|
|
310
|
-
setOllamaMode(
|
|
334
|
+
setOllamaMode(
|
|
335
|
+
editing.provider === 'ollama' && (Boolean(editing.credentialId) || isOllamaCloudModel(editing.model))
|
|
336
|
+
? 'cloud'
|
|
337
|
+
: 'local'
|
|
338
|
+
)
|
|
311
339
|
setOpenclawEnabled(editing.provider === 'openclaw')
|
|
312
340
|
setProjectId(editing.projectId)
|
|
313
341
|
setAvatarSeed(editing.avatarSeed || crypto.randomUUID().slice(0, 8))
|
|
@@ -316,6 +344,7 @@ export function AgentSheet() {
|
|
|
316
344
|
setMemoryScopeMode(editing.memoryScopeMode || 'auto')
|
|
317
345
|
setMemoryTierPreference(editing.memoryTierPreference || 'blended')
|
|
318
346
|
setAutoRecovery(editing.autoRecovery || false)
|
|
347
|
+
setDisabled(editing.disabled === true)
|
|
319
348
|
setVoiceId(editing.elevenLabsVoiceId || '')
|
|
320
349
|
setHeartbeatEnabled(editing.heartbeatEnabled || false)
|
|
321
350
|
setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
|
|
@@ -341,14 +370,7 @@ export function AgentSheet() {
|
|
|
341
370
|
setDailyBudget(typeof editing.dailyBudget === 'number' && editing.dailyBudget > 0 ? String(editing.dailyBudget) : '')
|
|
342
371
|
setMonthlyBudget(typeof editing.monthlyBudget === 'number' && editing.monthlyBudget > 0 ? String(editing.monthlyBudget) : '')
|
|
343
372
|
setBudgetAction(editing.budgetAction || 'warn')
|
|
344
|
-
|
|
345
|
-
if (editing.walletId) {
|
|
346
|
-
api<Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }>('GET', `/wallets/${editing.walletId}`)
|
|
347
|
-
.then(setAgentWallet)
|
|
348
|
-
.catch(() => setAgentWallet(null))
|
|
349
|
-
} else {
|
|
350
|
-
setAgentWallet(null)
|
|
351
|
-
}
|
|
373
|
+
void loadAgentWallets(editing.id)
|
|
352
374
|
} else {
|
|
353
375
|
setName('')
|
|
354
376
|
setDescription('')
|
|
@@ -383,6 +405,7 @@ export function AgentSheet() {
|
|
|
383
405
|
setMemoryScopeMode('auto')
|
|
384
406
|
setMemoryTierPreference('blended')
|
|
385
407
|
setAutoRecovery(false)
|
|
408
|
+
setDisabled(false)
|
|
386
409
|
setVoiceId('')
|
|
387
410
|
setHeartbeatEnabled(false)
|
|
388
411
|
setHeartbeatIntervalSec('')
|
|
@@ -404,6 +427,7 @@ export function AgentSheet() {
|
|
|
404
427
|
setDailyBudget('')
|
|
405
428
|
setMonthlyBudget('')
|
|
406
429
|
setBudgetAction('warn')
|
|
430
|
+
setAgentWallets([])
|
|
407
431
|
}
|
|
408
432
|
}
|
|
409
433
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -571,6 +595,7 @@ export function AgentSheet() {
|
|
|
571
595
|
memoryScopeMode,
|
|
572
596
|
memoryTierPreference,
|
|
573
597
|
autoRecovery,
|
|
598
|
+
disabled,
|
|
574
599
|
elevenLabsVoiceId: voiceId.trim() || null,
|
|
575
600
|
heartbeatEnabled,
|
|
576
601
|
heartbeatInterval: heartbeatIntervalSec ? formatHbDuration(Number(heartbeatIntervalSec)) : null,
|
|
@@ -744,9 +769,18 @@ export function AgentSheet() {
|
|
|
744
769
|
<BottomSheet open={open} onClose={onClose} wide>
|
|
745
770
|
<div className="mb-10 flex items-start justify-between">
|
|
746
771
|
<div>
|
|
747
|
-
<
|
|
748
|
-
|
|
749
|
-
|
|
772
|
+
<div className="mb-2 flex flex-wrap items-center gap-2">
|
|
773
|
+
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em]">
|
|
774
|
+
{editing ? 'Edit Agent' : 'New Agent'}
|
|
775
|
+
</h2>
|
|
776
|
+
<span className={`rounded-[999px] px-2.5 py-1 text-[10px] font-700 uppercase tracking-[0.1em] ${
|
|
777
|
+
disabled
|
|
778
|
+
? 'border border-amber-400/20 bg-amber-400/[0.08] text-amber-300'
|
|
779
|
+
: 'border border-emerald-400/20 bg-emerald-400/[0.08] text-emerald-300'
|
|
780
|
+
}`}>
|
|
781
|
+
{disabled ? 'Disabled' : 'Enabled'}
|
|
782
|
+
</span>
|
|
783
|
+
</div>
|
|
750
784
|
<p className="text-[14px] text-text-3">Define an AI agent and optional multi-agent delegation behavior</p>
|
|
751
785
|
</div>
|
|
752
786
|
<div className="flex items-center gap-3 mt-1.5">
|
|
@@ -1029,6 +1063,28 @@ export function AgentSheet() {
|
|
|
1029
1063
|
<p className="text-[11px] text-text-3/70 mt-1.5">Use working for fast recent context, durable for facts/preferences, and archive for long-lived history.</p>
|
|
1030
1064
|
</div>
|
|
1031
1065
|
|
|
1066
|
+
{/* Auto-Recovery */}
|
|
1067
|
+
<div className="mb-8">
|
|
1068
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
1069
|
+
<label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">
|
|
1070
|
+
Agent Availability
|
|
1071
|
+
<HintTip text="Disabled agents stay visible, but cannot take chats, heartbeats, schedules, or queued work until re-enabled." />
|
|
1072
|
+
</label>
|
|
1073
|
+
<div
|
|
1074
|
+
onClick={() => setDisabled(!disabled)}
|
|
1075
|
+
className={`w-9 h-5 rounded-full transition-all relative cursor-pointer ${disabled ? 'bg-amber-400' : 'bg-accent-bright'}`}
|
|
1076
|
+
>
|
|
1077
|
+
<div className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-all ${disabled ? 'left-0.5' : 'left-[18px]'}`} />
|
|
1078
|
+
</div>
|
|
1079
|
+
</div>
|
|
1080
|
+
<p className="text-[11px] text-text-3/70">
|
|
1081
|
+
{disabled
|
|
1082
|
+
? 'This agent is paused. Existing chats remain viewable, but new work is blocked until you switch it back on.'
|
|
1083
|
+
: 'This agent is active and can accept chats, heartbeats, schedules, and queued work.'}
|
|
1084
|
+
{' '}Gateway and remote runtime shutdown stay managed separately in Providers and OpenClaw Deploy.
|
|
1085
|
+
</p>
|
|
1086
|
+
</div>
|
|
1087
|
+
|
|
1032
1088
|
{/* Auto-Recovery */}
|
|
1033
1089
|
<div className="mb-8">
|
|
1034
1090
|
<div className="flex items-center justify-between mb-1.5">
|
|
@@ -1261,18 +1317,11 @@ export function AgentSheet() {
|
|
|
1261
1317
|
{editingId && (
|
|
1262
1318
|
<WalletSection
|
|
1263
1319
|
agentId={editingId}
|
|
1264
|
-
|
|
1320
|
+
wallets={agentWallets}
|
|
1321
|
+
activeWalletId={editing?.activeWalletId || editing?.walletId || agentWallets.find((wallet) => wallet.isActive)?.id || null}
|
|
1265
1322
|
onWalletCreated={async () => {
|
|
1266
1323
|
await loadAgents()
|
|
1267
|
-
|
|
1268
|
-
try {
|
|
1269
|
-
const wallets = await api<Record<string, Omit<AgentWallet, 'encryptedPrivateKey'>>>('GET', '/wallets')
|
|
1270
|
-
const match = Object.values(wallets).find((w) => w.agentId === editingId)
|
|
1271
|
-
if (match) {
|
|
1272
|
-
const detail = await api<Omit<AgentWallet, 'encryptedPrivateKey'> & { balanceLamports?: number; balanceSol?: number }>('GET', `/wallets/${match.id}`)
|
|
1273
|
-
setAgentWallet(detail)
|
|
1274
|
-
}
|
|
1275
|
-
} catch { /* ignore */ }
|
|
1324
|
+
await loadAgentWallets(editingId)
|
|
1276
1325
|
}}
|
|
1277
1326
|
/>
|
|
1278
1327
|
)}
|