@swarmclawai/swarmclaw 0.6.7 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -39
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +19 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/eval/run/route.ts +37 -0
- package/src/app/api/eval/scenarios/route.ts +24 -0
- package/src/app/api/eval/suite/route.ts +29 -0
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/graph/route.ts +46 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
- package/src/app/api/sessions/[id]/restore/route.ts +36 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/souls/[id]/route.ts +65 -0
- package/src/app/api/souls/route.ts +70 -0
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +16 -3
- package/src/app/api/tasks/route.ts +10 -2
- package/src/app/api/usage/route.ts +9 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +37 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +112 -34
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/agents/soul-library-picker.tsx +84 -13
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/activity-moment.tsx +2 -0
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/checkpoint-timeline.tsx +112 -0
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +46 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/session-debug-panel.tsx +106 -84
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/task-approval-card.tsx +78 -0
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-call-bubble.tsx +3 -0
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +37 -7
- package/src/components/home/home-view.tsx +54 -24
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +87 -19
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-browser.tsx +73 -45
- package/src/components/memory/memory-graph-view.tsx +203 -0
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +214 -60
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +28 -9
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/hint-tip.tsx +31 -0
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +224 -0
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +72 -48
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +319 -74
- package/src/lib/server/chatroom-helpers.ts +63 -5
- package/src/lib/server/chatroom-orchestration.ts +74 -0
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/context-manager.ts +132 -50
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +112 -1
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/eval/runner.ts +126 -0
- package/src/lib/server/eval/scenarios.ts +218 -0
- package/src/lib/server/eval/scorer.ts +96 -0
- package/src/lib/server/eval/store.ts +37 -0
- package/src/lib/server/eval/types.ts +48 -0
- package/src/lib/server/execution-log.ts +12 -8
- package/src/lib/server/guardian.ts +34 -0
- package/src/lib/server/heartbeat-service.ts +53 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/langgraph-checkpoint.ts +10 -0
- package/src/lib/server/link-understanding.ts +55 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +115 -16
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +193 -19
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/mmr.ts +73 -0
- package/src/lib/server/orchestrator-lg.ts +7 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +662 -132
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/query-expansion.ts +57 -0
- package/src/lib/server/queue.ts +280 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +32 -2
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +95 -33
- package/src/lib/server/session-tools/index.ts +217 -138
- package/src/lib/server/session-tools/memory.ts +154 -239
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +78 -0
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +181 -327
- package/src/lib/server/storage.ts +36 -0
- package/src/lib/server/stream-agent-chat.ts +348 -242
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +24 -5
- package/src/lib/server/tool-retry.ts +62 -0
- package/src/lib/server/transcript-repair.ts +72 -0
- package/src/lib/setup-defaults.ts +1 -0
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +24 -23
- package/src/lib/validation/schemas.ts +13 -0
- package/src/lib/view-routes.ts +2 -23
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +155 -10
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadSouls, saveSouls, deleteSoul, logActivity } from '@/lib/server/storage'
|
|
3
|
+
import { SOUL_LIBRARY } from '@/lib/soul-library'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
/** GET /api/souls/[id] */
|
|
9
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
10
|
+
const { id } = await params
|
|
11
|
+
|
|
12
|
+
// Check static library first
|
|
13
|
+
const staticSoul = SOUL_LIBRARY.find(s => s.id === id)
|
|
14
|
+
if (staticSoul) return NextResponse.json(staticSoul)
|
|
15
|
+
|
|
16
|
+
const souls = loadSouls()
|
|
17
|
+
if (!souls[id]) return NextResponse.json({ error: 'Soul not found' }, { status: 404 })
|
|
18
|
+
return NextResponse.json(souls[id])
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** PUT /api/souls/[id] — update custom soul */
|
|
22
|
+
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
23
|
+
const { id } = await params
|
|
24
|
+
const body = await req.json()
|
|
25
|
+
|
|
26
|
+
// Can only update custom souls
|
|
27
|
+
const souls = loadSouls()
|
|
28
|
+
if (!souls[id]) {
|
|
29
|
+
return NextResponse.json({ error: 'Only custom souls can be modified via this endpoint' }, { status: 403 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const updated = { ...souls[id], ...body, id, updatedAt: Date.now() }
|
|
33
|
+
souls[id] = updated
|
|
34
|
+
saveSouls(souls)
|
|
35
|
+
|
|
36
|
+
notify('souls')
|
|
37
|
+
return NextResponse.json(updated)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** DELETE /api/souls/[id] — delete custom soul */
|
|
41
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
42
|
+
const { id } = await params
|
|
43
|
+
|
|
44
|
+
// Only allow deleting custom ones
|
|
45
|
+
const souls = loadSouls()
|
|
46
|
+
if (!souls[id]) {
|
|
47
|
+
const isStatic = SOUL_LIBRARY.some(s => s.id === id)
|
|
48
|
+
if (isStatic) return NextResponse.json({ error: 'Cannot delete static library souls' }, { status: 403 })
|
|
49
|
+
return NextResponse.json({ error: 'Soul not found' }, { status: 404 })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const name = souls[id].name
|
|
53
|
+
deleteSoul(id)
|
|
54
|
+
|
|
55
|
+
logActivity({
|
|
56
|
+
entityType: 'soul',
|
|
57
|
+
entityId: id,
|
|
58
|
+
action: 'deleted',
|
|
59
|
+
actor: 'user',
|
|
60
|
+
summary: `Custom soul deleted: "${name}"`
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
notify('souls')
|
|
64
|
+
return NextResponse.json({ deleted: id })
|
|
65
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { SOUL_LIBRARY, type SoulTemplate } from '@/lib/soul-library'
|
|
3
|
+
import { loadSouls, saveSouls, logActivity } from '@/lib/server/storage'
|
|
4
|
+
import { genId } from '@/lib/id'
|
|
5
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
/** GET /api/souls — returns merged list of static library and custom user souls */
|
|
10
|
+
export async function GET(req: Request) {
|
|
11
|
+
const customSouls = loadSouls()
|
|
12
|
+
const { searchParams } = new URL(req.url)
|
|
13
|
+
const query = searchParams.get('q')?.toLowerCase() || ''
|
|
14
|
+
const archetype = searchParams.get('archetype')
|
|
15
|
+
|
|
16
|
+
const merged: SoulTemplate[] = [
|
|
17
|
+
...SOUL_LIBRARY,
|
|
18
|
+
...Object.values(customSouls) as SoulTemplate[],
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
let filtered = merged
|
|
22
|
+
if (archetype && archetype !== 'All') {
|
|
23
|
+
filtered = filtered.filter((s) => s.archetype === archetype)
|
|
24
|
+
}
|
|
25
|
+
if (query) {
|
|
26
|
+
filtered = filtered.filter(
|
|
27
|
+
(s) =>
|
|
28
|
+
s.name.toLowerCase().includes(query) ||
|
|
29
|
+
s.description.toLowerCase().includes(query) ||
|
|
30
|
+
s.tags.some((t) => t.toLowerCase().includes(query)) ||
|
|
31
|
+
s.soul.toLowerCase().includes(query),
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return NextResponse.json(filtered)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** POST /api/souls — create a custom soul */
|
|
39
|
+
export async function POST(req: Request) {
|
|
40
|
+
const body = await req.json()
|
|
41
|
+
if (!body.name || !body.soul) {
|
|
42
|
+
return NextResponse.json({ error: 'Name and soul content are required' }, { status: 400 })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const id = body.id || `custom-${genId()}`
|
|
46
|
+
const souls = loadSouls()
|
|
47
|
+
|
|
48
|
+
const newSoul: SoulTemplate = {
|
|
49
|
+
id,
|
|
50
|
+
name: body.name,
|
|
51
|
+
description: body.description || '',
|
|
52
|
+
soul: body.soul,
|
|
53
|
+
tags: Array.isArray(body.tags) ? body.tags : [],
|
|
54
|
+
archetype: body.archetype || 'Custom',
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
souls[id] = newSoul
|
|
58
|
+
saveSouls(souls)
|
|
59
|
+
|
|
60
|
+
logActivity({
|
|
61
|
+
entityType: 'soul',
|
|
62
|
+
entityId: id,
|
|
63
|
+
action: 'created',
|
|
64
|
+
actor: 'user',
|
|
65
|
+
summary: `Custom soul created: "${newSoul.name}"`
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
notify('souls')
|
|
69
|
+
return NextResponse.json(newSoul)
|
|
70
|
+
}
|
|
@@ -58,11 +58,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
58
58
|
saveTasks(t2)
|
|
59
59
|
notify('tasks')
|
|
60
60
|
}
|
|
61
|
-
} catch (err:
|
|
62
|
-
|
|
61
|
+
} catch (err: unknown) {
|
|
62
|
+
const errMsg = err instanceof Error ? err.message : String(err)
|
|
63
|
+
console.error(`[approve] Resume failed for task ${id}:`, errMsg)
|
|
63
64
|
const t2 = loadTasks()
|
|
64
65
|
if (t2[id]) {
|
|
65
|
-
t2[id].error =
|
|
66
|
+
t2[id].error = errMsg
|
|
66
67
|
t2[id].updatedAt = Date.now()
|
|
67
68
|
saveTasks(t2)
|
|
68
69
|
notify('tasks')
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { genId } from '@/lib/id'
|
|
2
2
|
import { NextResponse } from 'next/server'
|
|
3
|
-
import { loadTasks, saveTasks, logActivity } from '@/lib/server/storage'
|
|
3
|
+
import { loadTasks, saveTasks, logActivity, loadSettings } from '@/lib/server/storage'
|
|
4
4
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
-
import { disableSessionHeartbeat, enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
|
|
5
|
+
import { disableSessionHeartbeat, enqueueTask, recoverStalledRunningTasks, validateCompletedTasksQueue } from '@/lib/server/queue'
|
|
6
6
|
import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
|
|
7
7
|
import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
|
|
8
8
|
import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
@@ -11,10 +11,13 @@ import { createNotification } from '@/lib/server/create-notification'
|
|
|
11
11
|
import { enqueueSystemEvent } from '@/lib/server/system-events'
|
|
12
12
|
import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
|
|
13
13
|
import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
|
|
14
|
+
import { getPluginManager } from '@/lib/server/plugins'
|
|
15
|
+
import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
|
|
14
16
|
|
|
15
17
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
16
18
|
// Keep completed queue integrity even if daemon is not running.
|
|
17
19
|
validateCompletedTasksQueue()
|
|
20
|
+
recoverStalledRunningTasks()
|
|
18
21
|
|
|
19
22
|
const { id } = await params
|
|
20
23
|
const tasks = loadTasks()
|
|
@@ -25,6 +28,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
|
|
|
25
28
|
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
26
29
|
const { id } = await params
|
|
27
30
|
const body = await req.json()
|
|
31
|
+
const settings = loadSettings()
|
|
28
32
|
const tasks = loadTasks()
|
|
29
33
|
if (!tasks[id]) return notFound()
|
|
30
34
|
|
|
@@ -47,6 +51,11 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
47
51
|
tasks[id].comments.push(body.appendComment)
|
|
48
52
|
tasks[id].updatedAt = Date.now()
|
|
49
53
|
} else {
|
|
54
|
+
if (Object.prototype.hasOwnProperty.call(body, 'qualityGate')) {
|
|
55
|
+
body.qualityGate = body.qualityGate
|
|
56
|
+
? normalizeTaskQualityGate(body.qualityGate, settings)
|
|
57
|
+
: null
|
|
58
|
+
}
|
|
50
59
|
Object.assign(tasks[id], body, { updatedAt: Date.now() })
|
|
51
60
|
// Explicitly clear nullable fields when sent as null (Object.assign copies null but not undefined)
|
|
52
61
|
if (body.projectId === null) delete tasks[id].projectId
|
|
@@ -62,7 +71,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
62
71
|
if (tasks[id].status === 'completed') {
|
|
63
72
|
const report = ensureTaskCompletionReport(tasks[id])
|
|
64
73
|
if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
|
|
65
|
-
const validation = validateTaskCompletion(tasks[id], { report })
|
|
74
|
+
const validation = validateTaskCompletion(tasks[id], { report, settings })
|
|
66
75
|
tasks[id].validation = validation
|
|
67
76
|
if (validation.ok) {
|
|
68
77
|
tasks[id].completedAt = tasks[id].completedAt || Date.now()
|
|
@@ -100,6 +109,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
100
109
|
entityType: 'task',
|
|
101
110
|
entityId: id,
|
|
102
111
|
})
|
|
112
|
+
|
|
113
|
+
if (tasks[id].status === 'completed') {
|
|
114
|
+
getPluginManager().runHook('onTaskComplete', { taskId: id, result: tasks[id].result })
|
|
115
|
+
}
|
|
103
116
|
|
|
104
117
|
// Enqueue system event + heartbeat wake
|
|
105
118
|
if (tasks[id].sessionId) {
|
|
@@ -3,7 +3,7 @@ import { genId } from '@/lib/id'
|
|
|
3
3
|
import { loadTasks, saveTasks, loadSettings, loadAgents, logActivity } from '@/lib/server/storage'
|
|
4
4
|
import { TaskCreateSchema, formatZodError } from '@/lib/validation/schemas'
|
|
5
5
|
import { z } from 'zod'
|
|
6
|
-
import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
|
|
6
|
+
import { enqueueTask, recoverStalledRunningTasks, validateCompletedTasksQueue } from '@/lib/server/queue'
|
|
7
7
|
import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
|
|
8
8
|
import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
|
|
9
9
|
import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
@@ -11,10 +11,13 @@ import { notify } from '@/lib/server/ws-hub'
|
|
|
11
11
|
import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
|
|
12
12
|
import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
|
|
13
13
|
import { validateDag } from '@/lib/server/dag-validation'
|
|
14
|
+
import { getPluginManager } from '@/lib/server/plugins'
|
|
15
|
+
import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
|
|
14
16
|
|
|
15
17
|
export async function GET(req: Request) {
|
|
16
18
|
// Keep completed queue integrity even if daemon is not running.
|
|
17
19
|
validateCompletedTasksQueue()
|
|
20
|
+
recoverStalledRunningTasks()
|
|
18
21
|
|
|
19
22
|
const { searchParams } = new URL(req.url)
|
|
20
23
|
const includeArchived = searchParams.get('includeArchived') === 'true'
|
|
@@ -68,6 +71,9 @@ export async function POST(req: Request) {
|
|
|
68
71
|
const now = Date.now()
|
|
69
72
|
const tasks = loadTasks()
|
|
70
73
|
const settings = loadSettings()
|
|
74
|
+
const normalizedQualityGate = body.qualityGate
|
|
75
|
+
? normalizeTaskQualityGate(body.qualityGate, settings)
|
|
76
|
+
: null
|
|
71
77
|
const maxAttempts = Number.isFinite(Number(body.maxAttempts))
|
|
72
78
|
? Math.max(1, Math.min(20, Math.trunc(Number(body.maxAttempts))))
|
|
73
79
|
: Math.max(1, Math.min(20, Math.trunc(Number(settings.defaultTaskMaxAttempts ?? 3))))
|
|
@@ -146,6 +152,7 @@ export async function POST(req: Request) {
|
|
|
146
152
|
customFields: body.customFields && typeof body.customFields === 'object' ? body.customFields : undefined,
|
|
147
153
|
priority: ['low', 'medium', 'high', 'critical'].includes(body.priority) ? body.priority : undefined,
|
|
148
154
|
fingerprint: computeTaskFingerprint(body.title || 'Untitled Task', body.agentId || ''),
|
|
155
|
+
qualityGate: normalizedQualityGate,
|
|
149
156
|
}
|
|
150
157
|
|
|
151
158
|
// Dedup: if a non-terminal task with same fingerprint exists, return it
|
|
@@ -157,11 +164,12 @@ export async function POST(req: Request) {
|
|
|
157
164
|
if (tasks[id].status === 'completed') {
|
|
158
165
|
const report = ensureTaskCompletionReport(tasks[id])
|
|
159
166
|
if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
|
|
160
|
-
const validation = validateTaskCompletion(tasks[id], { report })
|
|
167
|
+
const validation = validateTaskCompletion(tasks[id], { report, settings })
|
|
161
168
|
tasks[id].validation = validation
|
|
162
169
|
if (validation.ok) {
|
|
163
170
|
tasks[id].completedAt = Date.now()
|
|
164
171
|
tasks[id].error = null
|
|
172
|
+
getPluginManager().runHook('onTaskComplete', { taskId: id, result: tasks[id].result })
|
|
165
173
|
} else {
|
|
166
174
|
tasks[id].status = 'failed'
|
|
167
175
|
tasks[id].completedAt = null
|
|
@@ -94,12 +94,14 @@ export async function GET(req: Request) {
|
|
|
94
94
|
errorCount: number
|
|
95
95
|
lastUsed: number
|
|
96
96
|
models: Set<string>
|
|
97
|
+
totalDurationMs: number
|
|
98
|
+
latencyCount: number
|
|
97
99
|
}> = {}
|
|
98
100
|
|
|
99
101
|
for (const r of records) {
|
|
100
102
|
const prov = r.provider || 'unknown'
|
|
101
103
|
if (!healthAccum[prov]) {
|
|
102
|
-
healthAccum[prov] = { totalRequests: 0, successCount: 0, errorCount: 0, lastUsed: 0, models: new Set() }
|
|
104
|
+
healthAccum[prov] = { totalRequests: 0, successCount: 0, errorCount: 0, lastUsed: 0, models: new Set(), totalDurationMs: 0, latencyCount: 0 }
|
|
103
105
|
}
|
|
104
106
|
const h = healthAccum[prov]
|
|
105
107
|
h.totalRequests += 1
|
|
@@ -107,6 +109,11 @@ export async function GET(req: Request) {
|
|
|
107
109
|
h.successCount += 1
|
|
108
110
|
if ((r.timestamp || 0) > h.lastUsed) h.lastUsed = r.timestamp || 0
|
|
109
111
|
if (r.model) h.models.add(r.model)
|
|
112
|
+
|
|
113
|
+
if (typeof r.durationMs === 'number' && r.durationMs > 0) {
|
|
114
|
+
h.totalDurationMs += r.durationMs
|
|
115
|
+
h.latencyCount += 1
|
|
116
|
+
}
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
const providerHealth: Record<string, {
|
|
@@ -125,7 +132,7 @@ export async function GET(req: Request) {
|
|
|
125
132
|
successCount: h.successCount,
|
|
126
133
|
errorCount: h.errorCount,
|
|
127
134
|
errorRate: h.totalRequests > 0 ? h.errorCount / h.totalRequests : 0,
|
|
128
|
-
avgLatencyMs: 0
|
|
135
|
+
avgLatencyMs: h.latencyCount > 0 ? h.totalDurationMs / h.latencyCount : 0,
|
|
129
136
|
lastUsed: h.lastUsed,
|
|
130
137
|
models: Array.from(h.models),
|
|
131
138
|
}
|
package/src/app/globals.css
CHANGED
|
@@ -171,6 +171,12 @@ textarea::-webkit-scrollbar { width: 0; }
|
|
|
171
171
|
textarea:hover { scrollbar-width: thin; }
|
|
172
172
|
textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
173
173
|
|
|
174
|
+
/* Improve scroll behavior on iOS/iPadOS nested panes */
|
|
175
|
+
.overflow-y-auto,
|
|
176
|
+
.overflow-auto {
|
|
177
|
+
-webkit-overflow-scrolling: touch;
|
|
178
|
+
}
|
|
179
|
+
|
|
174
180
|
/* Selection */
|
|
175
181
|
::selection { background: rgba(99,102,241,0.3); }
|
|
176
182
|
|
|
@@ -250,6 +256,23 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
250
256
|
0% { background-position: -200% center; }
|
|
251
257
|
100% { background-position: 200% center; }
|
|
252
258
|
}
|
|
259
|
+
@keyframes shimmer-bar {
|
|
260
|
+
0% { transform: translateX(-100%); }
|
|
261
|
+
100% { transform: translateX(100%); }
|
|
262
|
+
}
|
|
263
|
+
@keyframes spring-in {
|
|
264
|
+
0% { transform: scale(0.9) translateY(10px); opacity: 0; }
|
|
265
|
+
70% { transform: scale(1.02) translateY(-2px); opacity: 1; }
|
|
266
|
+
100% { transform: scale(1) translateY(0); opacity: 1; }
|
|
267
|
+
}
|
|
268
|
+
@keyframes pulse-subtle {
|
|
269
|
+
0%, 100% { opacity: 1; transform: scale(1); }
|
|
270
|
+
50% { opacity: 0.8; transform: scale(1.02); }
|
|
271
|
+
}
|
|
272
|
+
@keyframes glow-line {
|
|
273
|
+
0% { left: -100%; }
|
|
274
|
+
100% { left: 100%; }
|
|
275
|
+
}
|
|
253
276
|
@keyframes gradient-drift {
|
|
254
277
|
0% { background-position: 0% 50%; }
|
|
255
278
|
50% { background-position: 100% 50%; }
|
|
@@ -259,6 +282,10 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
259
282
|
from { opacity: 0; transform: translateY(10px); }
|
|
260
283
|
to { opacity: 1; transform: translateY(0); }
|
|
261
284
|
}
|
|
285
|
+
@keyframes fade-up {
|
|
286
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
287
|
+
to { opacity: 1; transform: translateY(0); }
|
|
288
|
+
}
|
|
262
289
|
|
|
263
290
|
/* Heartbeat float animation */
|
|
264
291
|
@keyframes heartbeat-float {
|
package/src/app/page.tsx
CHANGED
|
@@ -92,14 +92,14 @@ function FullScreenLoader() {
|
|
|
92
92
|
background: 'linear-gradient(135deg, rgba(255,255,255,0.6), rgba(129, 140, 248, 0.8))',
|
|
93
93
|
WebkitBackgroundClip: 'text',
|
|
94
94
|
WebkitTextFillColor: 'transparent',
|
|
95
|
-
animation: 'sc-text-fade 2s ease-in-out infinite alternate',
|
|
95
|
+
animation: 'sc-text-fade 2s ease-in-out infinite alternate, fade-up 0.6s var(--ease-spring) 0.2s both',
|
|
96
96
|
}}
|
|
97
97
|
>
|
|
98
98
|
SwarmClaw
|
|
99
99
|
</div>
|
|
100
100
|
|
|
101
101
|
{/* Loading bar */}
|
|
102
|
-
<div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden">
|
|
102
|
+
<div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
|
|
103
103
|
<div
|
|
104
104
|
className="h-full rounded-full bg-accent-bright/60"
|
|
105
105
|
style={{ animation: 'sc-progress 1.5s ease-in-out infinite' }}
|
|
@@ -150,7 +150,10 @@ export default function Home() {
|
|
|
150
150
|
|
|
151
151
|
const [authChecked, setAuthChecked] = useState(false)
|
|
152
152
|
const [authenticated, setAuthenticated] = useState(false)
|
|
153
|
-
const [setupDone, setSetupDone] = useState<boolean | null>(
|
|
153
|
+
const [setupDone, setSetupDone] = useState<boolean | null>(() => {
|
|
154
|
+
if (typeof window !== 'undefined' && localStorage.getItem('sc_setup_done') === '1') return true
|
|
155
|
+
return null
|
|
156
|
+
})
|
|
154
157
|
|
|
155
158
|
const checkAuth = useCallback(async () => {
|
|
156
159
|
const key = getStoredAccessKey()
|
|
@@ -252,7 +255,9 @@ export default function Home() {
|
|
|
252
255
|
])
|
|
253
256
|
if (cancelled) return
|
|
254
257
|
const hasCreds = Object.keys(creds).length > 0
|
|
255
|
-
|
|
258
|
+
const done = settings.setupCompleted === true || hasCreds
|
|
259
|
+
if (done) localStorage.setItem('sc_setup_done', '1')
|
|
260
|
+
setSetupDone(done)
|
|
256
261
|
} catch {
|
|
257
262
|
if (!cancelled) setSetupDone(true) // on error, skip wizard
|
|
258
263
|
}
|
|
@@ -285,6 +290,6 @@ export default function Home() {
|
|
|
285
290
|
if (!authenticated) return <AccessKeyGate onAuthenticated={() => setAuthenticated(true)} />
|
|
286
291
|
if (!currentUser) return <UserPicker />
|
|
287
292
|
if (setupDone === null || !agentReady) return <FullScreenLoader />
|
|
288
|
-
if (!setupDone) return <SetupWizard onComplete={() => setSetupDone(true)} />
|
|
293
|
+
if (!setupDone) return <SetupWizard onComplete={() => { localStorage.setItem('sc_setup_done', '1'); setSetupDone(true) }} />
|
|
289
294
|
return <AppLayout />
|
|
290
295
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -42,6 +42,14 @@ const COMMAND_GROUPS = [
|
|
|
42
42
|
}),
|
|
43
43
|
],
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
name: 'approvals',
|
|
47
|
+
description: 'Manage runtime approvals',
|
|
48
|
+
commands: [
|
|
49
|
+
cmd('list', 'GET', '/approvals', 'List pending approvals'),
|
|
50
|
+
cmd('resolve', 'POST', '/approvals', 'Approve/reject a pending approval', { expectsJsonBody: true }),
|
|
51
|
+
],
|
|
52
|
+
},
|
|
45
53
|
{
|
|
46
54
|
name: 'claude-skills',
|
|
47
55
|
description: 'Read local Claude skills directory metadata',
|
|
@@ -160,6 +168,16 @@ const COMMAND_GROUPS = [
|
|
|
160
168
|
cmd('delete', 'DELETE', '/documents/:id', 'Delete document'),
|
|
161
169
|
],
|
|
162
170
|
},
|
|
171
|
+
{
|
|
172
|
+
name: 'eval',
|
|
173
|
+
description: 'Run agent evaluation scenarios',
|
|
174
|
+
commands: [
|
|
175
|
+
cmd('scenarios', 'GET', '/eval/scenarios', 'List available eval scenarios'),
|
|
176
|
+
cmd('status', 'GET', '/eval/run', 'Get eval run status'),
|
|
177
|
+
cmd('run', 'POST', '/eval/run', 'Run an eval scenario against an agent', { expectsJsonBody: true }),
|
|
178
|
+
cmd('suite', 'POST', '/eval/suite', 'Run a full eval suite against an agent', { expectsJsonBody: true }),
|
|
179
|
+
],
|
|
180
|
+
},
|
|
163
181
|
{
|
|
164
182
|
name: 'files',
|
|
165
183
|
description: 'Serve and manage local files',
|
|
@@ -209,6 +227,7 @@ const COMMAND_GROUPS = [
|
|
|
209
227
|
cmd('delete', 'DELETE', '/memory/:id', 'Delete memory entry'),
|
|
210
228
|
cmd('maintenance', 'GET', '/memory/maintenance', 'Analyze memory dedupe/prune candidates'),
|
|
211
229
|
cmd('maintenance-run', 'POST', '/memory/maintenance', 'Run memory dedupe/prune maintenance', { expectsJsonBody: true }),
|
|
230
|
+
cmd('graph', 'GET', '/memory/graph', 'Get memory graph (nodes and links) for visualization'),
|
|
212
231
|
],
|
|
213
232
|
},
|
|
214
233
|
{
|
|
@@ -240,6 +259,8 @@ const COMMAND_GROUPS = [
|
|
|
240
259
|
cmd('delete', 'DELETE', '/mcp-servers/:id', 'Delete MCP server'),
|
|
241
260
|
cmd('test', 'POST', '/mcp-servers/:id/test', 'Test MCP server connection'),
|
|
242
261
|
cmd('tools', 'GET', '/mcp-servers/:id/tools', 'List tools available on an MCP server'),
|
|
262
|
+
cmd('conformance', 'POST', '/mcp-servers/:id/conformance', 'Run MCP conformance checks for a server', { expectsJsonBody: true }),
|
|
263
|
+
cmd('invoke', 'POST', '/mcp-servers/:id/invoke', 'Invoke an MCP tool on a server', { expectsJsonBody: true }),
|
|
243
264
|
],
|
|
244
265
|
},
|
|
245
266
|
{
|
|
@@ -320,8 +341,11 @@ const COMMAND_GROUPS = [
|
|
|
320
341
|
commands: [
|
|
321
342
|
cmd('list', 'GET', '/plugins', 'List installed plugins'),
|
|
322
343
|
cmd('set', 'POST', '/plugins', 'Enable/disable plugin', { expectsJsonBody: true }),
|
|
344
|
+
cmd('delete', 'DELETE', '/plugins', 'Delete an external plugin (use --query filename=plugin.js)'),
|
|
345
|
+
cmd('update', 'PATCH', '/plugins', 'Update a plugin (use --query id=plugin.js or --query all=true)'),
|
|
323
346
|
cmd('install', 'POST', '/plugins/install', 'Install plugin from URL', { expectsJsonBody: true }),
|
|
324
347
|
cmd('marketplace', 'GET', '/plugins/marketplace', 'Get marketplace catalog'),
|
|
348
|
+
cmd('ui', 'GET', '/plugins/ui', 'List plugin UI extensions (use --query type=sidebar|header|chat_actions|connectors)'),
|
|
325
349
|
],
|
|
326
350
|
},
|
|
327
351
|
{
|
|
@@ -428,6 +452,8 @@ const COMMAND_GROUPS = [
|
|
|
428
452
|
expectsJsonBody: true,
|
|
429
453
|
defaultBody: { action: 'status' },
|
|
430
454
|
}),
|
|
455
|
+
cmd('checkpoints', 'GET', '/sessions/:id/checkpoints', 'List checkpoint history for a session'),
|
|
456
|
+
cmd('restore', 'POST', '/sessions/:id/restore', 'Restore session to a previous checkpoint', { expectsJsonBody: true }),
|
|
431
457
|
],
|
|
432
458
|
},
|
|
433
459
|
{
|
|
@@ -459,6 +485,17 @@ const COMMAND_GROUPS = [
|
|
|
459
485
|
cmd('import', 'POST', '/skills/import', 'Import skill from URL', { expectsJsonBody: true }),
|
|
460
486
|
],
|
|
461
487
|
},
|
|
488
|
+
{
|
|
489
|
+
name: 'souls',
|
|
490
|
+
description: 'Browse and manage soul library templates',
|
|
491
|
+
commands: [
|
|
492
|
+
cmd('list', 'GET', '/souls', 'List soul templates'),
|
|
493
|
+
cmd('get', 'GET', '/souls/:id', 'Get soul template by id'),
|
|
494
|
+
cmd('create', 'POST', '/souls', 'Create custom soul template', { expectsJsonBody: true }),
|
|
495
|
+
cmd('update', 'PUT', '/souls/:id', 'Update soul template', { expectsJsonBody: true }),
|
|
496
|
+
cmd('delete', 'DELETE', '/souls/:id', 'Delete soul template'),
|
|
497
|
+
],
|
|
498
|
+
},
|
|
462
499
|
{
|
|
463
500
|
name: 'tasks',
|
|
464
501
|
description: 'Manage task board items',
|
|
@@ -65,8 +65,15 @@ export function ActivityFeed() {
|
|
|
65
65
|
<div className="text-center text-text-3 text-[14px] mt-16">No activity yet</div>
|
|
66
66
|
) : (
|
|
67
67
|
<div className="space-y-1">
|
|
68
|
-
{entries.map((entry: ActivityEntry) => (
|
|
69
|
-
<div
|
|
68
|
+
{entries.map((entry: ActivityEntry, idx: number) => (
|
|
69
|
+
<div
|
|
70
|
+
key={entry.id}
|
|
71
|
+
className="flex items-start gap-3 py-3 border-b border-white/[0.04]"
|
|
72
|
+
style={{
|
|
73
|
+
animation: 'fade-up 0.5s var(--ease-spring) both',
|
|
74
|
+
animationDelay: `${Math.min(idx * 0.03, 0.5)}s`
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
70
77
|
<div className="w-8 h-8 rounded-[8px] bg-surface-2 flex items-center justify-center text-[12px] font-700 text-text-3 shrink-0">
|
|
71
78
|
{ENTITY_ICONS[entry.entityType] || '?'}
|
|
72
79
|
</div>
|
|
@@ -38,7 +38,11 @@ export function AgentAvatar({ seed, avatarUrl, name, size = 32, className = '',
|
|
|
38
38
|
const dot = status && status !== 'idle' ? (
|
|
39
39
|
<span
|
|
40
40
|
className={`absolute -bottom-0.5 -right-0.5 rounded-full ${STATUS_COLORS[status]} ring-2 ring-[#0f0f1a]`}
|
|
41
|
-
style={{
|
|
41
|
+
style={{
|
|
42
|
+
width: dotSize,
|
|
43
|
+
height: dotSize,
|
|
44
|
+
animation: status === 'online' ? 'pulse-subtle 2s ease-in-out infinite' : undefined
|
|
45
|
+
}}
|
|
42
46
|
title={status === 'busy' ? 'Busy' : 'Online'}
|
|
43
47
|
/>
|
|
44
48
|
) : null
|
|
@@ -45,6 +45,22 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
|
|
|
45
45
|
const approvals = useApprovalStore((s) => s.approvals)
|
|
46
46
|
const pendingApprovalCount = Object.values(approvals).filter((a) => a.agentId === agent.id).length
|
|
47
47
|
const [heartbeatPulse, setHeartbeatPulse] = useState(false)
|
|
48
|
+
const monthlyBudget = typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0
|
|
49
|
+
? agent.monthlyBudget
|
|
50
|
+
: null
|
|
51
|
+
const hasMonthlyBudget = monthlyBudget !== null
|
|
52
|
+
const spendWindows = [
|
|
53
|
+
{
|
|
54
|
+
key: '1h',
|
|
55
|
+
spend: agent.hourlySpend ?? 0,
|
|
56
|
+
budget: typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0 ? agent.hourlyBudget : null,
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: '24h',
|
|
60
|
+
spend: agent.dailySpend ?? 0,
|
|
61
|
+
budget: typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0 ? agent.dailyBudget : null,
|
|
62
|
+
},
|
|
63
|
+
].filter((entry) => entry.budget !== null)
|
|
48
64
|
useWs(`heartbeat:agent:${agent.id}`, () => {
|
|
49
65
|
setHeartbeatPulse(true)
|
|
50
66
|
setTimeout(() => setHeartbeatPulse(false), 1500)
|
|
@@ -226,28 +242,58 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
|
|
|
226
242
|
<span>Cost: ${agent.totalCost.toFixed(2)}</span>
|
|
227
243
|
)}
|
|
228
244
|
</div>
|
|
229
|
-
{
|
|
245
|
+
{hasMonthlyBudget && (
|
|
230
246
|
<div className="mt-2">
|
|
231
247
|
<div className="flex items-center justify-between text-[10px] text-text-3/60 mb-1">
|
|
232
|
-
<span>${(agent.monthlySpend ?? 0).toFixed(2)} / ${
|
|
233
|
-
<span className={`font-600 ${(agent.monthlySpend ?? 0) >=
|
|
248
|
+
<span>${(agent.monthlySpend ?? 0).toFixed(2)} / ${monthlyBudget.toFixed(2)}</span>
|
|
249
|
+
<span className={`font-600 ${(agent.monthlySpend ?? 0) >= monthlyBudget ? 'text-red-400' : 'text-text-3/50'}`}>
|
|
234
250
|
{agent.budgetAction === 'block' ? 'hard cap' : 'soft cap'}
|
|
235
251
|
</span>
|
|
236
252
|
</div>
|
|
237
|
-
<div className="h-1 rounded-full bg-white/[0.06] overflow-hidden">
|
|
253
|
+
<div className="h-1 rounded-full bg-white/[0.06] overflow-hidden relative">
|
|
238
254
|
<div
|
|
239
|
-
className={`h-full rounded-full transition-all duration-300 ${
|
|
240
|
-
(agent.monthlySpend ?? 0) >=
|
|
255
|
+
className={`h-full rounded-full transition-all duration-300 relative ${
|
|
256
|
+
(agent.monthlySpend ?? 0) >= monthlyBudget
|
|
241
257
|
? 'bg-red-400'
|
|
242
|
-
: (agent.monthlySpend ?? 0) >=
|
|
258
|
+
: (agent.monthlySpend ?? 0) >= monthlyBudget * 0.8
|
|
243
259
|
? 'bg-amber-400'
|
|
244
260
|
: 'bg-accent'
|
|
245
261
|
}`}
|
|
246
|
-
style={{ width: `${Math.min(100, ((agent.monthlySpend ?? 0) /
|
|
247
|
-
|
|
262
|
+
style={{ width: `${Math.min(100, ((agent.monthlySpend ?? 0) / monthlyBudget) * 100)}%` }}
|
|
263
|
+
>
|
|
264
|
+
{/* Shimmer overlay for active feel */}
|
|
265
|
+
<div
|
|
266
|
+
className="absolute inset-0 w-full h-full bg-gradient-to-r from-transparent via-white/20 to-transparent"
|
|
267
|
+
style={{ animation: 'shimmer-bar 2s linear infinite' }}
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
248
270
|
</div>
|
|
249
271
|
</div>
|
|
250
272
|
)}
|
|
273
|
+
{spendWindows.length > 0 && (
|
|
274
|
+
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
275
|
+
{spendWindows.map((entry) => {
|
|
276
|
+
const budget = entry.budget as number
|
|
277
|
+
const ratio = budget > 0 ? (entry.spend / budget) : 0
|
|
278
|
+
const overCap = ratio >= 1
|
|
279
|
+
const nearCap = !overCap && ratio >= 0.8
|
|
280
|
+
return (
|
|
281
|
+
<span
|
|
282
|
+
key={entry.key}
|
|
283
|
+
className={`text-[10px] px-2 py-0.5 rounded-[6px] border ${
|
|
284
|
+
overCap
|
|
285
|
+
? 'text-red-400 border-red-400/25 bg-red-400/[0.06]'
|
|
286
|
+
: nearCap
|
|
287
|
+
? 'text-amber-400 border-amber-400/20 bg-amber-400/[0.06]'
|
|
288
|
+
: 'text-text-3/70 border-white/[0.08] bg-white/[0.03]'
|
|
289
|
+
}`}
|
|
290
|
+
>
|
|
291
|
+
{entry.key}: ${entry.spend.toFixed(2)} / ${budget.toFixed(2)}
|
|
292
|
+
</span>
|
|
293
|
+
)
|
|
294
|
+
})}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
251
297
|
</div>
|
|
252
298
|
|
|
253
299
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|