@swarmclawai/swarmclaw 0.5.2 → 0.5.3
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 +5 -1
- package/package.json +1 -1
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +64 -0
- package/src/app/api/search/route.ts +105 -0
- package/src/app/api/tasks/[id]/route.ts +8 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/cli/index.js +18 -0
- package/src/cli/spec.js +16 -0
- package/src/components/layout/app-layout.tsx +46 -0
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/schedules/schedule-card.tsx +2 -1
- package/src/components/shared/notification-center.tsx +185 -0
- package/src/components/shared/search-dialog.tsx +287 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/server/create-notification.ts +30 -0
- package/src/lib/server/daemon-state.ts +8 -0
- package/src/lib/server/storage.ts +27 -0
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +58 -1
- package/src/types/index.ts +13 -0
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ Inspired by [OpenClaw](https://github.com/openclaw).
|
|
|
20
20
|
- Always use the access key authentication (generated on first run)
|
|
21
21
|
- Never expose port 3456 without a reverse proxy + TLS
|
|
22
22
|
- Review agent system prompts before giving them shell or browser tools
|
|
23
|
+
- Repeated failed access key attempts are rate-limited to slow brute-force attacks
|
|
23
24
|
|
|
24
25
|
## Features
|
|
25
26
|
|
|
@@ -40,6 +41,8 @@ Inspired by [OpenClaw](https://github.com/openclaw).
|
|
|
40
41
|
- **Session Run Queue** — Per-session queued runs with followup/steer/collect modes, collect coalescing for bursty inputs, and run-state APIs
|
|
41
42
|
- **Chat Iteration Workflow** — Edit-and-resend user turns, fork a new session from any message, bookmark key messages, use contextual follow-up suggestion chips, and auto-continue after tool access grants
|
|
42
43
|
- **Live Chat Telemetry** — Thinking/tool/responding stream phases, live main-loop status badges, connector activity presence, tone indicator, and optional sound notifications
|
|
44
|
+
- **Global Search Palette** — `Cmd/Ctrl+K` search across agents, tasks, sessions, schedules, webhooks, and skills from anywhere in the app
|
|
45
|
+
- **Notification Center** — Real-time in-app notifications for task/schedule/daemon events with unread tracking and quick actions
|
|
43
46
|
- **Preview-Rich Chat UI** — Side preview panel for tool outputs (image/browser/html/code), inline code/PDF previews for attachments, and image lightbox support
|
|
44
47
|
- **Voice Settings** — Per-instance ElevenLabs API key + voice ID for TTS replies, plus configurable speech recognition language for chat input
|
|
45
48
|
- **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling
|
|
@@ -48,6 +51,7 @@ Inspired by [OpenClaw](https://github.com/openclaw).
|
|
|
48
51
|
- **Context Management** — Auto-compaction of conversation history when approaching context limits, with manual `context_status` and `context_summarize` tools for agents
|
|
49
52
|
- **Memory** — Per-agent and per-session memory with hybrid FTS5 + vector embeddings search, relevance-based memory recall injected into runs, and periodic auto-journaling for durable execution context
|
|
50
53
|
- **Cost Tracking** — Per-message token counting and cost estimation displayed in the chat header
|
|
54
|
+
- **Provider Health Metrics** — Usage dashboard surfaces provider request volume, success rates, models used, and last-used timestamps
|
|
51
55
|
- **Model Failover** — Automatic key rotation on rate limits and auth errors with configurable fallback credentials
|
|
52
56
|
- **Plugin System** — Extend agent behavior with JS plugins (hooks: beforeAgentStart, afterAgentComplete, beforeToolExec, afterToolExec, onMessage)
|
|
53
57
|
- **Secrets Vault** — Encrypted storage for API keys and service tokens
|
|
@@ -81,7 +85,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
81
85
|
```
|
|
82
86
|
|
|
83
87
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
84
|
-
To pin a version: `SWARMCLAW_VERSION=v0.5.
|
|
88
|
+
To pin a version: `SWARMCLAW_VERSION=v0.5.3 curl ... | bash`
|
|
85
89
|
|
|
86
90
|
Or run locally from the repo (friendly for non-technical users):
|
|
87
91
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadNotifications, markNotificationRead, deleteNotification } from '@/lib/server/storage'
|
|
3
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
4
|
+
|
|
5
|
+
export async function PUT(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
|
+
const { id } = await params
|
|
7
|
+
const all = loadNotifications()
|
|
8
|
+
if (!all[id]) {
|
|
9
|
+
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
markNotificationRead(id)
|
|
13
|
+
notify('notifications')
|
|
14
|
+
return NextResponse.json({ ok: true })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
18
|
+
const { id } = await params
|
|
19
|
+
const all = loadNotifications()
|
|
20
|
+
if (!all[id]) {
|
|
21
|
+
return NextResponse.json({ error: 'Not found' }, { status: 404 })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
deleteNotification(id)
|
|
25
|
+
notify('notifications')
|
|
26
|
+
return NextResponse.json({ ok: true })
|
|
27
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { loadNotifications, saveNotification, deleteNotification } from '@/lib/server/storage'
|
|
4
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
+
import type { AppNotification } from '@/types'
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
export async function GET(req: Request) {
|
|
9
|
+
const { searchParams } = new URL(req.url)
|
|
10
|
+
const unreadOnly = searchParams.get('unreadOnly') === 'true'
|
|
11
|
+
const limit = Math.min(200, Math.max(1, Number(searchParams.get('limit')) || 100))
|
|
12
|
+
|
|
13
|
+
const all = loadNotifications()
|
|
14
|
+
let entries = Object.values(all) as AppNotification[]
|
|
15
|
+
|
|
16
|
+
if (unreadOnly) {
|
|
17
|
+
entries = entries.filter((e) => !e.read)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
entries.sort((a, b) => b.createdAt - a.createdAt)
|
|
21
|
+
entries = entries.slice(0, limit)
|
|
22
|
+
|
|
23
|
+
return NextResponse.json(entries)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function POST(req: Request) {
|
|
27
|
+
const body = (await req.json()) as Record<string, unknown>
|
|
28
|
+
const id = genId()
|
|
29
|
+
const notification: AppNotification = {
|
|
30
|
+
id,
|
|
31
|
+
type: (['info', 'success', 'warning', 'error'].includes(body.type as string) ? body.type : 'info') as AppNotification['type'],
|
|
32
|
+
title: typeof body.title === 'string' ? body.title : 'Notification',
|
|
33
|
+
message: typeof body.message === 'string' ? body.message : undefined,
|
|
34
|
+
entityType: typeof body.entityType === 'string' ? body.entityType : undefined,
|
|
35
|
+
entityId: typeof body.entityId === 'string' ? body.entityId : undefined,
|
|
36
|
+
read: false,
|
|
37
|
+
createdAt: Date.now(),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
saveNotification(id, notification)
|
|
41
|
+
notify('notifications')
|
|
42
|
+
return NextResponse.json(notification, { status: 201 })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function DELETE(req: Request) {
|
|
46
|
+
const { searchParams } = new URL(req.url)
|
|
47
|
+
const specificId = searchParams.get('id')
|
|
48
|
+
|
|
49
|
+
if (specificId) {
|
|
50
|
+
deleteNotification(specificId)
|
|
51
|
+
} else {
|
|
52
|
+
// Clear all read notifications
|
|
53
|
+
const all = loadNotifications()
|
|
54
|
+
for (const raw of Object.values(all)) {
|
|
55
|
+
const entry = raw as AppNotification
|
|
56
|
+
if (entry.read) {
|
|
57
|
+
deleteNotification(entry.id)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
notify('notifications')
|
|
63
|
+
return NextResponse.json({ ok: true })
|
|
64
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import {
|
|
3
|
+
loadTasks,
|
|
4
|
+
loadAgents,
|
|
5
|
+
loadSessions,
|
|
6
|
+
loadSchedules,
|
|
7
|
+
loadWebhooks,
|
|
8
|
+
loadSkills,
|
|
9
|
+
} from '@/lib/server/storage'
|
|
10
|
+
|
|
11
|
+
interface SearchResult {
|
|
12
|
+
type: 'task' | 'agent' | 'session' | 'schedule' | 'webhook' | 'skill'
|
|
13
|
+
id: string
|
|
14
|
+
title: string
|
|
15
|
+
description?: string
|
|
16
|
+
status?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const MAX_RESULTS = 20
|
|
20
|
+
|
|
21
|
+
function matches(haystack: string | undefined | null, needle: string): boolean {
|
|
22
|
+
if (!haystack) return false
|
|
23
|
+
return haystack.toLowerCase().includes(needle)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function searchCollection(
|
|
27
|
+
collection: Record<string, Record<string, unknown>>,
|
|
28
|
+
type: SearchResult['type'],
|
|
29
|
+
needle: string,
|
|
30
|
+
titleKey: string,
|
|
31
|
+
descKey: string,
|
|
32
|
+
statusKey?: string,
|
|
33
|
+
): SearchResult[] {
|
|
34
|
+
const results: SearchResult[] = []
|
|
35
|
+
for (const [id, item] of Object.entries(collection)) {
|
|
36
|
+
const title = item[titleKey] as string | undefined
|
|
37
|
+
const desc = item[descKey] as string | undefined
|
|
38
|
+
const idStr = typeof item.id === 'string' ? item.id : id
|
|
39
|
+
if (matches(title, needle) || matches(desc, needle) || matches(idStr, needle)) {
|
|
40
|
+
results.push({
|
|
41
|
+
type,
|
|
42
|
+
id,
|
|
43
|
+
title: title || idStr || id,
|
|
44
|
+
description: desc ? desc.slice(0, 120) : undefined,
|
|
45
|
+
status: statusKey ? (item[statusKey] as string | undefined) : undefined,
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return results
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function GET(req: Request) {
|
|
53
|
+
const { searchParams } = new URL(req.url)
|
|
54
|
+
const q = (searchParams.get('q') || '').trim().toLowerCase()
|
|
55
|
+
|
|
56
|
+
if (q.length < 2) {
|
|
57
|
+
return NextResponse.json({ results: [] })
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const tasks = loadTasks() as Record<string, Record<string, unknown>>
|
|
61
|
+
const agents = loadAgents() as Record<string, Record<string, unknown>>
|
|
62
|
+
const sessions = loadSessions() as Record<string, Record<string, unknown>>
|
|
63
|
+
const schedules = loadSchedules() as Record<string, Record<string, unknown>>
|
|
64
|
+
const webhooks = loadWebhooks() as Record<string, Record<string, unknown>>
|
|
65
|
+
const skills = loadSkills() as Record<string, Record<string, unknown>>
|
|
66
|
+
|
|
67
|
+
const buckets: SearchResult[][] = [
|
|
68
|
+
searchCollection(agents, 'agent', q, 'name', 'description'),
|
|
69
|
+
searchCollection(tasks, 'task', q, 'title', 'description', 'status'),
|
|
70
|
+
searchCollection(sessions, 'session', q, 'name', 'cwd'),
|
|
71
|
+
searchCollection(schedules, 'schedule', q, 'name', 'taskPrompt', 'status'),
|
|
72
|
+
searchCollection(webhooks, 'webhook', q, 'name', 'source'),
|
|
73
|
+
searchCollection(skills, 'skill', q, 'name', 'description'),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
// Proportional allocation across types
|
|
77
|
+
const totalRaw = buckets.reduce((s, b) => s + b.length, 0)
|
|
78
|
+
if (totalRaw === 0) {
|
|
79
|
+
return NextResponse.json({ results: [] })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const results: SearchResult[] = []
|
|
83
|
+
if (totalRaw <= MAX_RESULTS) {
|
|
84
|
+
for (const bucket of buckets) results.push(...bucket)
|
|
85
|
+
} else {
|
|
86
|
+
// Give each bucket a fair share, round-robin leftover slots
|
|
87
|
+
const perBucket = Math.floor(MAX_RESULTS / buckets.length)
|
|
88
|
+
let remaining = MAX_RESULTS
|
|
89
|
+
for (const bucket of buckets) {
|
|
90
|
+
const take = Math.min(bucket.length, perBucket)
|
|
91
|
+
results.push(...bucket.slice(0, take))
|
|
92
|
+
remaining -= take
|
|
93
|
+
}
|
|
94
|
+
// Fill remaining slots from buckets that had more
|
|
95
|
+
for (const bucket of buckets) {
|
|
96
|
+
if (remaining <= 0) break
|
|
97
|
+
const alreadyTaken = Math.min(bucket.length, perBucket)
|
|
98
|
+
const extra = bucket.slice(alreadyTaken, alreadyTaken + remaining)
|
|
99
|
+
results.push(...extra)
|
|
100
|
+
remaining -= extra.length
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return NextResponse.json({ results })
|
|
105
|
+
}
|
|
@@ -7,6 +7,7 @@ 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'
|
|
9
9
|
import { notify } from '@/lib/server/ws-hub'
|
|
10
|
+
import { createNotification } from '@/lib/server/create-notification'
|
|
10
11
|
|
|
11
12
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
12
13
|
// Keep completed queue integrity even if daemon is not running.
|
|
@@ -76,6 +77,13 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
|
|
|
76
77
|
// If task is manually transitioned to a terminal status, disable session heartbeat.
|
|
77
78
|
if (prevStatus !== tasks[id].status && (tasks[id].status === 'completed' || tasks[id].status === 'failed')) {
|
|
78
79
|
disableSessionHeartbeat(tasks[id].sessionId)
|
|
80
|
+
createNotification({
|
|
81
|
+
type: tasks[id].status === 'completed' ? 'success' : 'error',
|
|
82
|
+
title: `Task ${tasks[id].status}: "${tasks[id].title}"`,
|
|
83
|
+
message: tasks[id].status === 'failed' ? tasks[id].error?.slice(0, 200) : undefined,
|
|
84
|
+
entityType: 'task',
|
|
85
|
+
entityId: id,
|
|
86
|
+
})
|
|
79
87
|
}
|
|
80
88
|
|
|
81
89
|
// Dependency check: cannot queue a task if any blocker is incomplete
|
|
@@ -78,6 +78,50 @@ export async function GET(req: Request) {
|
|
|
78
78
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
79
79
|
.map(([bucket, data]) => ({ bucket, tokens: data.tokens, cost: Math.round(data.cost * 10000) / 10000 }))
|
|
80
80
|
|
|
81
|
+
// Provider health stats
|
|
82
|
+
const healthAccum: Record<string, {
|
|
83
|
+
totalRequests: number
|
|
84
|
+
successCount: number
|
|
85
|
+
errorCount: number
|
|
86
|
+
lastUsed: number
|
|
87
|
+
models: Set<string>
|
|
88
|
+
}> = {}
|
|
89
|
+
|
|
90
|
+
for (const r of records) {
|
|
91
|
+
const prov = r.provider || 'unknown'
|
|
92
|
+
if (!healthAccum[prov]) {
|
|
93
|
+
healthAccum[prov] = { totalRequests: 0, successCount: 0, errorCount: 0, lastUsed: 0, models: new Set() }
|
|
94
|
+
}
|
|
95
|
+
const h = healthAccum[prov]
|
|
96
|
+
h.totalRequests += 1
|
|
97
|
+
// UsageRecord has no error/status field — logged records are successful completions
|
|
98
|
+
h.successCount += 1
|
|
99
|
+
if ((r.timestamp || 0) > h.lastUsed) h.lastUsed = r.timestamp || 0
|
|
100
|
+
if (r.model) h.models.add(r.model)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const providerHealth: Record<string, {
|
|
104
|
+
totalRequests: number
|
|
105
|
+
successCount: number
|
|
106
|
+
errorCount: number
|
|
107
|
+
errorRate: number
|
|
108
|
+
avgLatencyMs: number
|
|
109
|
+
lastUsed: number
|
|
110
|
+
models: string[]
|
|
111
|
+
}> = {}
|
|
112
|
+
|
|
113
|
+
for (const [prov, h] of Object.entries(healthAccum)) {
|
|
114
|
+
providerHealth[prov] = {
|
|
115
|
+
totalRequests: h.totalRequests,
|
|
116
|
+
successCount: h.successCount,
|
|
117
|
+
errorCount: h.errorCount,
|
|
118
|
+
errorRate: h.totalRequests > 0 ? h.errorCount / h.totalRequests : 0,
|
|
119
|
+
avgLatencyMs: 0, // UsageRecord does not track latency
|
|
120
|
+
lastUsed: h.lastUsed,
|
|
121
|
+
models: Array.from(h.models),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
81
125
|
return NextResponse.json({
|
|
82
126
|
records,
|
|
83
127
|
totalTokens,
|
|
@@ -85,5 +129,6 @@ export async function GET(req: Request) {
|
|
|
85
129
|
byAgent,
|
|
86
130
|
byProvider,
|
|
87
131
|
timeSeries,
|
|
132
|
+
providerHealth,
|
|
88
133
|
})
|
|
89
134
|
}
|
package/src/cli/index.js
CHANGED
|
@@ -183,6 +183,17 @@ const COMMAND_GROUPS = [
|
|
|
183
183
|
cmd('get', 'GET', '/memory-images/:filename', 'Download memory image by filename', { responseType: 'binary' }),
|
|
184
184
|
],
|
|
185
185
|
},
|
|
186
|
+
{
|
|
187
|
+
name: 'notifications',
|
|
188
|
+
description: 'Manage in-app notifications',
|
|
189
|
+
commands: [
|
|
190
|
+
cmd('list', 'GET', '/notifications', 'List notifications (use --query unreadOnly=true --query limit=100)'),
|
|
191
|
+
cmd('create', 'POST', '/notifications', 'Create notification', { expectsJsonBody: true }),
|
|
192
|
+
cmd('clear', 'DELETE', '/notifications', 'Clear read notifications'),
|
|
193
|
+
cmd('mark-read', 'PUT', '/notifications/:id', 'Mark notification as read'),
|
|
194
|
+
cmd('delete', 'DELETE', '/notifications/:id', 'Delete notification by id'),
|
|
195
|
+
],
|
|
196
|
+
},
|
|
186
197
|
{
|
|
187
198
|
name: 'mcp-servers',
|
|
188
199
|
description: 'Manage MCP server configurations',
|
|
@@ -293,6 +304,13 @@ const COMMAND_GROUPS = [
|
|
|
293
304
|
cmd('models-clear', 'DELETE', '/providers/:id/models', 'Clear provider model overrides'),
|
|
294
305
|
],
|
|
295
306
|
},
|
|
307
|
+
{
|
|
308
|
+
name: 'search',
|
|
309
|
+
description: 'Global search across app resources',
|
|
310
|
+
commands: [
|
|
311
|
+
cmd('query', 'GET', '/search', 'Search agents/tasks/sessions/schedules/webhooks/skills (use --query q=term)'),
|
|
312
|
+
],
|
|
313
|
+
},
|
|
296
314
|
{
|
|
297
315
|
name: 'runs',
|
|
298
316
|
description: 'Session run queue/history',
|
package/src/cli/spec.js
CHANGED
|
@@ -130,6 +130,16 @@ const COMMAND_GROUPS = {
|
|
|
130
130
|
get: { description: 'Download memory image by filename', method: 'GET', path: '/memory-images/:filename', params: ['filename'], binary: true },
|
|
131
131
|
},
|
|
132
132
|
},
|
|
133
|
+
notifications: {
|
|
134
|
+
description: 'In-app notification center',
|
|
135
|
+
commands: {
|
|
136
|
+
list: { description: 'List notifications (supports --query unreadOnly=true,limit=100)', method: 'GET', path: '/notifications' },
|
|
137
|
+
create: { description: 'Create notification', method: 'POST', path: '/notifications' },
|
|
138
|
+
clear: { description: 'Clear read notifications', method: 'DELETE', path: '/notifications' },
|
|
139
|
+
'mark-read': { description: 'Mark notification as read', method: 'PUT', path: '/notifications/:id', params: ['id'] },
|
|
140
|
+
delete: { description: 'Delete notification by id', method: 'DELETE', path: '/notifications/:id', params: ['id'] },
|
|
141
|
+
},
|
|
142
|
+
},
|
|
133
143
|
orchestrator: {
|
|
134
144
|
description: 'Orchestrator runs and run-state APIs',
|
|
135
145
|
commands: {
|
|
@@ -198,6 +208,12 @@ const COMMAND_GROUPS = {
|
|
|
198
208
|
'models-reset': { description: 'Delete provider model overrides', method: 'DELETE', path: '/providers/:id/models', params: ['id'] },
|
|
199
209
|
},
|
|
200
210
|
},
|
|
211
|
+
search: {
|
|
212
|
+
description: 'Global search across app resources',
|
|
213
|
+
commands: {
|
|
214
|
+
query: { description: 'Search agents/tasks/sessions/schedules/webhooks/skills (supports --query q=term)', method: 'GET', path: '/search' },
|
|
215
|
+
},
|
|
216
|
+
},
|
|
201
217
|
schedules: {
|
|
202
218
|
description: 'Scheduled task automation',
|
|
203
219
|
commands: {
|
|
@@ -40,10 +40,12 @@ import { ActivityFeed } from '@/components/activity/activity-feed'
|
|
|
40
40
|
import { MetricsDashboard } from '@/components/usage/metrics-dashboard'
|
|
41
41
|
import { ProjectList } from '@/components/projects/project-list'
|
|
42
42
|
import { ProjectSheet } from '@/components/projects/project-sheet'
|
|
43
|
+
import { SearchDialog } from '@/components/shared/search-dialog'
|
|
43
44
|
import { NetworkBanner } from './network-banner'
|
|
44
45
|
import { UpdateBanner } from './update-banner'
|
|
45
46
|
import { MobileHeader } from './mobile-header'
|
|
46
47
|
import { DaemonIndicator } from './daemon-indicator'
|
|
48
|
+
import { NotificationCenter } from '@/components/shared/notification-center'
|
|
47
49
|
import { ChatArea } from '@/components/chat/chat-area'
|
|
48
50
|
import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'
|
|
49
51
|
import type { AppView } from '@/types'
|
|
@@ -238,6 +240,37 @@ export function AppLayout() {
|
|
|
238
240
|
</RailTooltip>
|
|
239
241
|
)}
|
|
240
242
|
|
|
243
|
+
{/* Search */}
|
|
244
|
+
{railExpanded ? (
|
|
245
|
+
<div className="px-3 mb-2">
|
|
246
|
+
<button
|
|
247
|
+
onClick={() => window.dispatchEvent(new CustomEvent('swarmclaw:open-search'))}
|
|
248
|
+
className="w-full flex items-center gap-2.5 px-3 py-2 rounded-[10px] text-[13px] font-500 cursor-pointer transition-all
|
|
249
|
+
bg-transparent text-text-3 hover:text-text hover:bg-white/[0.04] border-none"
|
|
250
|
+
style={{ fontFamily: 'inherit' }}
|
|
251
|
+
>
|
|
252
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
253
|
+
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
254
|
+
</svg>
|
|
255
|
+
Search
|
|
256
|
+
<kbd className="ml-auto px-1.5 py-0.5 rounded-[5px] bg-white/[0.06] border border-white/[0.08] text-[10px] font-mono text-text-3">
|
|
257
|
+
⌘K
|
|
258
|
+
</kbd>
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
) : (
|
|
262
|
+
<RailTooltip label="Search" description="Search across all entities (⌘K)">
|
|
263
|
+
<button
|
|
264
|
+
onClick={() => window.dispatchEvent(new CustomEvent('swarmclaw:open-search'))}
|
|
265
|
+
className="rail-btn self-center mb-2"
|
|
266
|
+
>
|
|
267
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
268
|
+
<circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
|
|
269
|
+
</svg>
|
|
270
|
+
</button>
|
|
271
|
+
</RailTooltip>
|
|
272
|
+
)}
|
|
273
|
+
|
|
241
274
|
{/* Nav items */}
|
|
242
275
|
<div className={`flex flex-col gap-0.5 ${railExpanded ? 'px-3' : 'items-center'}`}>
|
|
243
276
|
<NavItem view="agents" label="Agents" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('agents')}>
|
|
@@ -363,6 +396,18 @@ export function AppLayout() {
|
|
|
363
396
|
</RailTooltip>
|
|
364
397
|
)}
|
|
365
398
|
{railExpanded && <DaemonIndicator />}
|
|
399
|
+
{railExpanded ? (
|
|
400
|
+
<div className="flex items-center gap-1 px-3 py-1">
|
|
401
|
+
<span className="text-[12px] font-500 text-text-3 flex-1">Alerts</span>
|
|
402
|
+
<NotificationCenter />
|
|
403
|
+
</div>
|
|
404
|
+
) : (
|
|
405
|
+
<RailTooltip label="Notifications" description="View system notifications">
|
|
406
|
+
<div className="rail-btn flex items-center justify-center">
|
|
407
|
+
<NotificationCenter />
|
|
408
|
+
</div>
|
|
409
|
+
</RailTooltip>
|
|
410
|
+
)}
|
|
366
411
|
{railExpanded ? (
|
|
367
412
|
<button
|
|
368
413
|
onClick={() => handleNavClick('settings')}
|
|
@@ -644,6 +689,7 @@ export function AppLayout() {
|
|
|
644
689
|
</div>
|
|
645
690
|
</ErrorBoundary>
|
|
646
691
|
|
|
692
|
+
<SearchDialog />
|
|
647
693
|
<AgentSheet />
|
|
648
694
|
<ScheduleSheet />
|
|
649
695
|
<MemorySheet />
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useAppStore } from '@/stores/use-app-store'
|
|
4
4
|
import { IconButton } from '@/components/shared/icon-button'
|
|
5
|
+
import { NotificationCenter } from '@/components/shared/notification-center'
|
|
5
6
|
|
|
6
7
|
export function MobileHeader() {
|
|
7
8
|
const toggleSidebar = useAppStore((s) => s.toggleSidebar)
|
|
@@ -26,6 +27,7 @@ export function MobileHeader() {
|
|
|
26
27
|
<span className="font-700">SwarmClaw</span>
|
|
27
28
|
)}
|
|
28
29
|
</h1>
|
|
30
|
+
<NotificationCenter />
|
|
29
31
|
</header>
|
|
30
32
|
)
|
|
31
33
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { Schedule } from '@/types'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
|
+
import { cronToHuman } from '@/lib/cron-human'
|
|
6
7
|
|
|
7
8
|
const STATUS_COLORS: Record<string, string> = {
|
|
8
9
|
active: 'text-emerald-400 bg-emerald-400/[0.08]',
|
|
@@ -95,7 +96,7 @@ export function ScheduleCard({ schedule, inSidebar }: Props) {
|
|
|
95
96
|
<div className="text-[12px] text-text-3/70 mt-1.5 truncate">
|
|
96
97
|
{agent?.name || 'Unknown agent'} · {schedule.scheduleType}
|
|
97
98
|
{!inSidebar && schedule.scheduleType === 'cron' && schedule.cron && (
|
|
98
|
-
<span className="
|
|
99
|
+
<span className="text-text-3/50 ml-1" title={schedule.cron}>({cronToHuman(schedule.cron)})</span>
|
|
99
100
|
)}
|
|
100
101
|
{!inSidebar && schedule.scheduleType === 'interval' && schedule.intervalMs && (
|
|
101
102
|
<span className="text-text-3/50 ml-1">
|