@swarmclawai/swarmclaw 0.6.0 → 0.6.2
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 +15 -2
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +455 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +180 -7
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +68 -16
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/stream-agent-chat.ts +32 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
|
@@ -3,40 +3,7 @@ import { notFound } from '@/lib/server/collection-helpers'
|
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
import { UPLOAD_DIR } from '@/lib/server/storage'
|
|
6
|
-
|
|
7
|
-
const MIME_TYPES: Record<string, string> = {
|
|
8
|
-
'.png': 'image/png',
|
|
9
|
-
'.jpg': 'image/jpeg',
|
|
10
|
-
'.jpeg': 'image/jpeg',
|
|
11
|
-
'.gif': 'image/gif',
|
|
12
|
-
'.webp': 'image/webp',
|
|
13
|
-
'.svg': 'image/svg+xml',
|
|
14
|
-
'.bmp': 'image/bmp',
|
|
15
|
-
'.ico': 'image/x-icon',
|
|
16
|
-
'.mp4': 'video/mp4',
|
|
17
|
-
'.webm': 'video/webm',
|
|
18
|
-
'.mov': 'video/quicktime',
|
|
19
|
-
'.avi': 'video/x-msvideo',
|
|
20
|
-
'.mkv': 'video/x-matroska',
|
|
21
|
-
'.pdf': 'application/pdf',
|
|
22
|
-
'.json': 'application/json',
|
|
23
|
-
'.csv': 'text/csv',
|
|
24
|
-
'.txt': 'text/plain',
|
|
25
|
-
'.html': 'text/html',
|
|
26
|
-
'.xml': 'application/xml',
|
|
27
|
-
'.zip': 'application/zip',
|
|
28
|
-
'.tar': 'application/x-tar',
|
|
29
|
-
'.gz': 'application/gzip',
|
|
30
|
-
'.doc': 'application/msword',
|
|
31
|
-
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
32
|
-
'.xls': 'application/vnd.ms-excel',
|
|
33
|
-
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
34
|
-
'.ppt': 'application/vnd.ms-powerpoint',
|
|
35
|
-
'.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
36
|
-
'.mp3': 'audio/mpeg',
|
|
37
|
-
'.wav': 'audio/wav',
|
|
38
|
-
'.ogg': 'audio/ogg',
|
|
39
|
-
}
|
|
6
|
+
import { MIME_TYPES } from '@/lib/server/mime'
|
|
40
7
|
|
|
41
8
|
export async function GET(_req: Request, { params }: { params: Promise<{ filename: string }> }) {
|
|
42
9
|
const { filename } = await params
|
|
@@ -60,3 +27,21 @@ export async function GET(_req: Request, { params }: { params: Promise<{ filenam
|
|
|
60
27
|
},
|
|
61
28
|
})
|
|
62
29
|
}
|
|
30
|
+
|
|
31
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ filename: string }> }) {
|
|
32
|
+
const { filename } = await params
|
|
33
|
+
const safeName = filename.replace(/[^a-zA-Z0-9._-]/g, '')
|
|
34
|
+
|
|
35
|
+
if (safeName.includes('..') || safeName.includes('/')) {
|
|
36
|
+
return NextResponse.json({ error: 'Invalid filename' }, { status: 400 })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const filePath = path.join(UPLOAD_DIR, safeName)
|
|
40
|
+
|
|
41
|
+
if (!fs.existsSync(filePath)) {
|
|
42
|
+
return notFound()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fs.unlinkSync(filePath)
|
|
46
|
+
return NextResponse.json({ ok: true })
|
|
47
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { UPLOAD_DIR } from '@/lib/server/storage'
|
|
5
|
+
import { getFileCategory } from '@/lib/server/mime'
|
|
6
|
+
|
|
7
|
+
interface UploadFile {
|
|
8
|
+
name: string
|
|
9
|
+
size: number
|
|
10
|
+
modified: number
|
|
11
|
+
category: string
|
|
12
|
+
url: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function listUploadFiles(): UploadFile[] {
|
|
16
|
+
if (!fs.existsSync(UPLOAD_DIR)) return []
|
|
17
|
+
const entries = fs.readdirSync(UPLOAD_DIR)
|
|
18
|
+
const files: UploadFile[] = []
|
|
19
|
+
for (const name of entries) {
|
|
20
|
+
const filePath = path.join(UPLOAD_DIR, name)
|
|
21
|
+
try {
|
|
22
|
+
const stat = fs.statSync(filePath)
|
|
23
|
+
if (!stat.isFile()) continue
|
|
24
|
+
const ext = path.extname(name).toLowerCase()
|
|
25
|
+
files.push({
|
|
26
|
+
name,
|
|
27
|
+
size: stat.size,
|
|
28
|
+
modified: stat.mtimeMs,
|
|
29
|
+
category: getFileCategory(ext),
|
|
30
|
+
url: `/api/uploads/${encodeURIComponent(name)}`,
|
|
31
|
+
})
|
|
32
|
+
} catch {
|
|
33
|
+
// skip files we can't stat
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return files
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function GET() {
|
|
40
|
+
const files = listUploadFiles()
|
|
41
|
+
const totalSize = files.reduce((sum, f) => sum + f.size, 0)
|
|
42
|
+
return NextResponse.json({ files, totalSize, count: files.length })
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface DeleteBody {
|
|
46
|
+
filenames?: string[]
|
|
47
|
+
olderThanDays?: number
|
|
48
|
+
category?: string
|
|
49
|
+
all?: boolean
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isUnsafeName(name: string): boolean {
|
|
53
|
+
return name.includes('/') || name.includes('\\') || name.includes('..')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function DELETE(req: Request) {
|
|
57
|
+
const body = (await req.json()) as DeleteBody
|
|
58
|
+
const files = listUploadFiles()
|
|
59
|
+
let toDelete: string[] = []
|
|
60
|
+
|
|
61
|
+
if (body.all) {
|
|
62
|
+
toDelete = files.map((f) => f.name)
|
|
63
|
+
} else if (body.filenames && Array.isArray(body.filenames)) {
|
|
64
|
+
for (const name of body.filenames) {
|
|
65
|
+
if (typeof name !== 'string' || isUnsafeName(name)) {
|
|
66
|
+
return NextResponse.json({ error: `Invalid filename: ${name}` }, { status: 400 })
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
toDelete = body.filenames
|
|
70
|
+
} else if (typeof body.olderThanDays === 'number') {
|
|
71
|
+
const cutoff = Date.now() - body.olderThanDays * 86_400_000
|
|
72
|
+
toDelete = files.filter((f) => f.modified < cutoff).map((f) => f.name)
|
|
73
|
+
} else if (typeof body.category === 'string') {
|
|
74
|
+
toDelete = files.filter((f) => f.category === body.category).map((f) => f.name)
|
|
75
|
+
} else {
|
|
76
|
+
return NextResponse.json({ error: 'Provide filenames, olderThanDays, category, or all' }, { status: 400 })
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let deleted = 0
|
|
80
|
+
let freedBytes = 0
|
|
81
|
+
for (const name of toDelete) {
|
|
82
|
+
const filePath = path.join(UPLOAD_DIR, name)
|
|
83
|
+
try {
|
|
84
|
+
const stat = fs.statSync(filePath)
|
|
85
|
+
fs.unlinkSync(filePath)
|
|
86
|
+
freedBytes += stat.size
|
|
87
|
+
deleted++
|
|
88
|
+
} catch {
|
|
89
|
+
// file already gone or inaccessible
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return NextResponse.json({ deleted, freedBytes })
|
|
94
|
+
}
|
package/src/app/globals.css
CHANGED
|
@@ -269,6 +269,11 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
|
269
269
|
to { opacity: 0; transform: translateY(-8px) scale(0.95); }
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
+
@keyframes delegation-handoff-in {
|
|
273
|
+
0% { opacity: 0; transform: translateX(16px) scale(0.95); }
|
|
274
|
+
100% { opacity: 1; transform: translateX(0) scale(1); }
|
|
275
|
+
}
|
|
276
|
+
|
|
272
277
|
@keyframes avatar-moment-pulse {
|
|
273
278
|
0% { transform: scale(1); }
|
|
274
279
|
30% { transform: scale(1.15); }
|
package/src/cli/index.js
CHANGED
|
@@ -79,6 +79,14 @@ const COMMAND_GROUPS = [
|
|
|
79
79
|
}),
|
|
80
80
|
],
|
|
81
81
|
},
|
|
82
|
+
{
|
|
83
|
+
name: 'canvas',
|
|
84
|
+
description: 'Read/update per-session canvas content',
|
|
85
|
+
commands: [
|
|
86
|
+
cmd('get', 'GET', '/canvas/:sessionId', 'Get current canvas content for a session'),
|
|
87
|
+
cmd('set', 'POST', '/canvas/:sessionId', 'Set/clear canvas content for a session', { expectsJsonBody: true }),
|
|
88
|
+
],
|
|
89
|
+
},
|
|
82
90
|
{
|
|
83
91
|
name: 'connectors',
|
|
84
92
|
description: 'Manage chat connectors',
|
|
@@ -154,6 +162,7 @@ const COMMAND_GROUPS = [
|
|
|
154
162
|
description: 'Serve and manage local files',
|
|
155
163
|
commands: [
|
|
156
164
|
cmd('serve', 'GET', '/files/serve', 'Serve a local file (use --query path=/abs/path)'),
|
|
165
|
+
cmd('open', 'POST', '/files/open', 'Open a local file path via the host default app/browser', { expectsJsonBody: true }),
|
|
157
166
|
],
|
|
158
167
|
},
|
|
159
168
|
{
|
|
@@ -381,6 +390,8 @@ const COMMAND_GROUPS = [
|
|
|
381
390
|
}),
|
|
382
391
|
cmd('messages', 'GET', '/sessions/:id/messages', 'Get session messages'),
|
|
383
392
|
cmd('messages-update', 'PUT', '/sessions/:id/messages', 'Update session message metadata (e.g. bookmark)', { expectsJsonBody: true }),
|
|
393
|
+
cmd('messages-send', 'POST', '/sessions/:id/messages', 'Append a user/system message to a session', { expectsJsonBody: true }),
|
|
394
|
+
cmd('messages-delete', 'DELETE', '/sessions/:id/messages', 'Delete a message from a session', { expectsJsonBody: true }),
|
|
384
395
|
cmd('fork', 'POST', '/sessions/:id/fork', 'Fork session from a specific message index', { expectsJsonBody: true }),
|
|
385
396
|
cmd('edit-resend', 'POST', '/sessions/:id/edit-resend', 'Edit and resend from a specific message index', { expectsJsonBody: true }),
|
|
386
397
|
cmd('main-loop', 'GET', '/sessions/:id/main-loop', 'Get main mission loop state'),
|
|
@@ -455,6 +466,7 @@ const COMMAND_GROUPS = [
|
|
|
455
466
|
cmd('delete', 'DELETE', '/tasks/:id', 'Delete task'),
|
|
456
467
|
cmd('purge', 'DELETE', '/tasks', 'Bulk delete tasks', { expectsJsonBody: true }),
|
|
457
468
|
cmd('approve', 'POST', '/tasks/:id/approve', 'Approve or reject a pending tool execution', { expectsJsonBody: true }),
|
|
469
|
+
cmd('metrics', 'GET', '/tasks/metrics', 'Get task board metrics (supports --query range=24h|7d|30d)'),
|
|
458
470
|
],
|
|
459
471
|
},
|
|
460
472
|
{
|
|
@@ -485,9 +497,12 @@ const COMMAND_GROUPS = [
|
|
|
485
497
|
},
|
|
486
498
|
{
|
|
487
499
|
name: 'uploads',
|
|
488
|
-
description: '
|
|
500
|
+
description: 'Manage uploaded artifacts',
|
|
489
501
|
commands: [
|
|
502
|
+
cmd('list', 'GET', '/uploads', 'List uploaded artifacts'),
|
|
490
503
|
cmd('get', 'GET', '/uploads/:filename', 'Download uploaded artifact', { responseType: 'binary' }),
|
|
504
|
+
cmd('delete', 'DELETE', '/uploads/:filename', 'Delete uploaded artifact by filename'),
|
|
505
|
+
cmd('delete-many', 'DELETE', '/uploads', 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', { expectsJsonBody: true }),
|
|
491
506
|
],
|
|
492
507
|
},
|
|
493
508
|
{
|
package/src/cli/spec.js
CHANGED
|
@@ -40,6 +40,13 @@ const COMMAND_GROUPS = {
|
|
|
40
40
|
pin: { description: 'Toggle pin on a chatroom message', method: 'POST', path: '/chatrooms/:id/pins', params: ['id'] },
|
|
41
41
|
},
|
|
42
42
|
},
|
|
43
|
+
canvas: {
|
|
44
|
+
description: 'Session canvas content',
|
|
45
|
+
commands: {
|
|
46
|
+
get: { description: 'Get current canvas content for a session', method: 'GET', path: '/canvas/:sessionId', params: ['sessionId'] },
|
|
47
|
+
set: { description: 'Set/clear canvas content for a session', method: 'POST', path: '/canvas/:sessionId', params: ['sessionId'] },
|
|
48
|
+
},
|
|
49
|
+
},
|
|
43
50
|
connectors: {
|
|
44
51
|
description: 'Manage chat connectors',
|
|
45
52
|
commands: {
|
|
@@ -120,6 +127,22 @@ const COMMAND_GROUPS = {
|
|
|
120
127
|
},
|
|
121
128
|
},
|
|
122
129
|
},
|
|
130
|
+
uploads: {
|
|
131
|
+
description: 'Manage uploaded artifacts',
|
|
132
|
+
commands: {
|
|
133
|
+
list: { description: 'List uploaded artifacts', method: 'GET', path: '/uploads' },
|
|
134
|
+
get: { description: 'Download uploaded artifact by filename', method: 'GET', path: '/uploads/:filename', params: ['filename'], binary: true },
|
|
135
|
+
delete: { description: 'Delete uploaded artifact by filename', method: 'DELETE', path: '/uploads/:filename', params: ['filename'] },
|
|
136
|
+
'delete-many': { description: 'Delete uploads by filter/body (filenames, olderThanDays, category, or all)', method: 'DELETE', path: '/uploads' },
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
files: {
|
|
140
|
+
description: 'Serve/open local files',
|
|
141
|
+
commands: {
|
|
142
|
+
serve: { description: 'Serve a local file (supports --query path=/some/file)', method: 'GET', path: '/files/serve' },
|
|
143
|
+
open: { description: 'Open a local file path via host default app/browser', method: 'POST', path: '/files/open' },
|
|
144
|
+
},
|
|
145
|
+
},
|
|
123
146
|
logs: {
|
|
124
147
|
description: 'Application logs',
|
|
125
148
|
commands: {
|
|
@@ -262,6 +285,8 @@ const COMMAND_GROUPS = {
|
|
|
262
285
|
'heartbeat-disable-all': { description: 'Disable all session heartbeats and cancel queued heartbeat runs', method: 'POST', path: '/sessions/heartbeat' },
|
|
263
286
|
messages: { description: 'Get session message history', method: 'GET', path: '/sessions/:id/messages', params: ['id'] },
|
|
264
287
|
'messages-update': { description: 'Update session message metadata (e.g. bookmark)', method: 'PUT', path: '/sessions/:id/messages', params: ['id'] },
|
|
288
|
+
'messages-send': { description: 'Append a user/system message to a session', method: 'POST', path: '/sessions/:id/messages', params: ['id'] },
|
|
289
|
+
'messages-delete': { description: 'Delete a message from a session', method: 'DELETE', path: '/sessions/:id/messages', params: ['id'] },
|
|
265
290
|
fork: { description: 'Fork session from a specific message index', method: 'POST', path: '/sessions/:id/fork', params: ['id'] },
|
|
266
291
|
'edit-resend': { description: 'Edit and resend from a specific message index', method: 'POST', path: '/sessions/:id/edit-resend', params: ['id'] },
|
|
267
292
|
'main-loop': { description: 'Get main mission loop state for a session', method: 'GET', path: '/sessions/:id/main-loop', params: ['id'] },
|
|
@@ -323,6 +348,7 @@ const COMMAND_GROUPS = {
|
|
|
323
348
|
delete: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
|
|
324
349
|
archive: { description: 'Archive task', method: 'DELETE', path: '/tasks/:id', params: ['id'] },
|
|
325
350
|
approve: { description: 'Approve or reject a pending tool execution', method: 'POST', path: '/tasks/:id/approve', params: ['id'] },
|
|
351
|
+
metrics: { description: 'Get task board metrics (supports --query range=24h|7d|30d)', method: 'GET', path: '/tasks/metrics' },
|
|
326
352
|
},
|
|
327
353
|
},
|
|
328
354
|
webhooks: {
|
|
@@ -164,8 +164,8 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
|
|
|
164
164
|
/>
|
|
165
165
|
<span className="font-display text-[14px] font-600 truncate flex-1 tracking-[-0.01em]">{agent.name}</span>
|
|
166
166
|
{pendingApprovalCount > 0 && (
|
|
167
|
-
<span className="shrink-0
|
|
168
|
-
{pendingApprovalCount}
|
|
167
|
+
<span className="shrink-0 text-[9px] font-600 uppercase tracking-wider px-2 py-0.5 rounded-[6px] text-amber-400 bg-amber-400/[0.08] border border-amber-400/15">
|
|
168
|
+
{pendingApprovalCount} {pendingApprovalCount === 1 ? 'approval' : 'approvals'}
|
|
169
169
|
</span>
|
|
170
170
|
)}
|
|
171
171
|
{isDefault && (
|
|
@@ -178,7 +178,7 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
|
|
|
178
178
|
onClick={handleRunClick}
|
|
179
179
|
disabled={running}
|
|
180
180
|
className="shrink-0 text-[10px] font-600 uppercase tracking-wider px-2.5 py-1 rounded-[6px] cursor-pointer
|
|
181
|
-
transition-all border-none bg-accent-bright/20 text-
|
|
181
|
+
transition-all border-none bg-accent-bright/20 text-accent-bright hover:bg-accent-bright/30 disabled:opacity-40"
|
|
182
182
|
style={{ fontFamily: 'inherit' }}
|
|
183
183
|
>
|
|
184
184
|
{running ? '...' : 'Run'}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
6
|
+
import { useChatroomStore } from '@/stores/use-chatroom-store'
|
|
6
7
|
import { fetchMessages } from '@/lib/sessions'
|
|
7
8
|
import type { Agent, Session } from '@/types'
|
|
8
9
|
import { AgentAvatar } from './agent-avatar'
|
|
@@ -28,6 +29,8 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
|
|
|
28
29
|
const streamingSessionId = useChatStore((s) => s.streamingSessionId)
|
|
29
30
|
const chatFilter = useAppStore((s) => s.chatFilter ?? 'all')
|
|
30
31
|
const setChatFilter = useAppStore((s) => s.setChatFilter)
|
|
32
|
+
const chatrooms = useChatroomStore((s) => s.chatrooms)
|
|
33
|
+
const chatroomStreaming = useChatroomStore((s) => s.streamingAgents)
|
|
31
34
|
const [search, setSearch] = useState('')
|
|
32
35
|
|
|
33
36
|
// FLIP animation refs
|
|
@@ -57,6 +60,21 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
|
|
|
57
60
|
})
|
|
58
61
|
}, [agents, sessions, search])
|
|
59
62
|
|
|
63
|
+
// Compute agents active in chatrooms (message in last 30min or currently streaming)
|
|
64
|
+
const chatroomActiveAgentIds = useMemo(() => {
|
|
65
|
+
const set = new Set<string>()
|
|
66
|
+
const cutoff = Date.now() - 30 * 60 * 1000
|
|
67
|
+
for (const chatroom of Object.values(chatrooms)) {
|
|
68
|
+
for (let i = chatroom.messages.length - 1; i >= 0; i--) {
|
|
69
|
+
const msg = chatroom.messages[i]
|
|
70
|
+
if (msg.time < cutoff) break
|
|
71
|
+
if (msg.role === 'assistant' && msg.senderId !== 'user') set.add(msg.senderId)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const agentId of chatroomStreaming.keys()) set.add(agentId)
|
|
75
|
+
return set
|
|
76
|
+
}, [chatrooms, chatroomStreaming])
|
|
77
|
+
|
|
60
78
|
// Compute running tasks per agent
|
|
61
79
|
const runningAgentIds = useMemo(() => {
|
|
62
80
|
const set = new Set<string>()
|
|
@@ -74,12 +92,13 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
|
|
|
74
92
|
const threadSession = a.threadSessionId ? sessions[a.threadSessionId] as Session | undefined : undefined
|
|
75
93
|
const isRunning = runningAgentIds.has(a.id) || (threadSession?.active ?? false)
|
|
76
94
|
const isStreaming = streamingSessionId === a.threadSessionId
|
|
77
|
-
|
|
95
|
+
const isChatroomActive = chatroomActiveAgentIds.has(a.id)
|
|
96
|
+
if (chatFilter === 'active') return isRunning || isStreaming || isChatroomActive
|
|
78
97
|
// 'recent' — activity within 24h
|
|
79
98
|
const lastActive = threadSession?.lastActiveAt || a.updatedAt
|
|
80
99
|
return now - lastActive < 86_400_000
|
|
81
100
|
})
|
|
82
|
-
}, [sortedAgents, chatFilter, sessions, runningAgentIds, streamingSessionId])
|
|
101
|
+
}, [sortedAgents, chatFilter, sessions, runningAgentIds, streamingSessionId, chatroomActiveAgentIds])
|
|
83
102
|
|
|
84
103
|
// FLIP: animate row position changes
|
|
85
104
|
useLayoutEffect(() => {
|
|
@@ -109,7 +128,9 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
|
|
|
109
128
|
try {
|
|
110
129
|
const msgs = await fetchMessages(state.currentSessionId)
|
|
111
130
|
setMessages(msgs)
|
|
112
|
-
} catch
|
|
131
|
+
} catch (err: unknown) {
|
|
132
|
+
console.error('[agent-chat-list] Failed to load messages:', err instanceof Error ? err.message : String(err))
|
|
133
|
+
}
|
|
113
134
|
}
|
|
114
135
|
onSelect?.()
|
|
115
136
|
// Delay scroll so React renders the new messages first
|
|
@@ -186,7 +207,7 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
|
|
|
186
207
|
const isActive = currentAgentId === agent.id
|
|
187
208
|
const heartbeatOn = agent.heartbeatEnabled === true && (agent.tools?.length ?? 0) > 0
|
|
188
209
|
const recentlyActive = (threadSession?.lastActiveAt ?? 0) > Date.now() - 30 * 60 * 1000
|
|
189
|
-
const isWorking = runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive
|
|
210
|
+
const isWorking = runningAgentIds.has(agent.id) || (threadSession?.active ?? false) || heartbeatOn || recentlyActive || chatroomActiveAgentIds.has(agent.id)
|
|
190
211
|
const isTyping = streamingSessionId === agent.threadSessionId
|
|
191
212
|
const preview = lastMsg?.text?.slice(0, 80)?.replace(/\n/g, ' ') || ''
|
|
192
213
|
|
|
@@ -194,7 +215,7 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
|
|
|
194
215
|
<div
|
|
195
216
|
key={agent.id}
|
|
196
217
|
ref={(el) => setRowRef(agent.id, el)}
|
|
197
|
-
className={`group/row relative w-full text-left py-3 px-
|
|
218
|
+
className={`group/row relative w-full text-left py-3 px-4 rounded-[12px] cursor-pointer transition-all duration-150 border-none
|
|
198
219
|
${isActive
|
|
199
220
|
? 'bg-accent-soft/80 border border-accent-bright/20'
|
|
200
221
|
: 'bg-transparent hover:bg-white/[0.02]'}`}
|
|
@@ -213,7 +234,9 @@ export function AgentChatList({ inSidebar, onSelect }: Props) {
|
|
|
213
234
|
{agent.name}
|
|
214
235
|
</span>
|
|
215
236
|
<span className="text-[10px] text-text-3/60 font-mono shrink-0">
|
|
216
|
-
{
|
|
237
|
+
{(threadSession?.model || agent.model)
|
|
238
|
+
? (threadSession?.model || agent.model)!.split('/').pop()?.split(':')[0]
|
|
239
|
+
: agent.provider}
|
|
217
240
|
</span>
|
|
218
241
|
{/* Set as default agent */}
|
|
219
242
|
{(() => {
|
|
@@ -14,8 +14,9 @@ import { AgentAvatar } from './agent-avatar'
|
|
|
14
14
|
import { AgentPickerList } from '@/components/shared/agent-picker-list'
|
|
15
15
|
import { randomSoul } from '@/lib/soul-suggestions'
|
|
16
16
|
import { SectionLabel } from '@/components/shared/section-label'
|
|
17
|
+
import { SoulLibraryPicker } from './soul-library-picker'
|
|
17
18
|
|
|
18
|
-
const HB_PRESETS = [
|
|
19
|
+
const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
|
|
19
20
|
|
|
20
21
|
function formatHbDuration(sec: number): string {
|
|
21
22
|
if (sec >= 3600) {
|
|
@@ -63,6 +64,7 @@ export function AgentSheet() {
|
|
|
63
64
|
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
64
65
|
const credentials = useAppStore((s) => s.credentials)
|
|
65
66
|
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
67
|
+
const appSettings = useAppStore((s) => s.appSettings)
|
|
66
68
|
const dynamicSkills = useAppStore((s) => s.skills)
|
|
67
69
|
const mcpServers = useAppStore((s) => s.mcpServers)
|
|
68
70
|
const loadSkills = useAppStore((s) => s.loadSkills)
|
|
@@ -80,6 +82,8 @@ export function AgentSheet() {
|
|
|
80
82
|
const [name, setName] = useState('')
|
|
81
83
|
const [description, setDescription] = useState('')
|
|
82
84
|
const [soul, setSoul] = useState('')
|
|
85
|
+
const [soulInitial, setSoulInitial] = useState('')
|
|
86
|
+
const [soulSaveState, setSoulSaveState] = useState<'idle' | 'saved'>('idle')
|
|
83
87
|
const [systemPrompt, setSystemPrompt] = useState('')
|
|
84
88
|
const [provider, setProvider] = useState<ProviderType>('claude-cli')
|
|
85
89
|
const [model, setModel] = useState('')
|
|
@@ -95,7 +99,6 @@ export function AgentSheet() {
|
|
|
95
99
|
const [mcpTools, setMcpTools] = useState<Record<string, { name: string; description: string }[]>>({})
|
|
96
100
|
const [mcpToolsLoading, setMcpToolsLoading] = useState(false)
|
|
97
101
|
const [fallbackCredentialIds, setFallbackCredentialIds] = useState<string[]>([])
|
|
98
|
-
// platformAssignScope is derived from isOrchestrator — no separate state needed
|
|
99
102
|
const [capabilities, setCapabilities] = useState<string[]>([])
|
|
100
103
|
const [capInput, setCapInput] = useState('')
|
|
101
104
|
const [ollamaMode, setOllamaMode] = useState<'local' | 'cloud'>('local')
|
|
@@ -103,6 +106,7 @@ export function AgentSheet() {
|
|
|
103
106
|
const [projectId, setProjectId] = useState<string | undefined>(undefined)
|
|
104
107
|
const [avatarSeed, setAvatarSeed] = useState('')
|
|
105
108
|
const [thinkingLevel, setThinkingLevel] = useState<'' | 'minimal' | 'low' | 'medium' | 'high'>('')
|
|
109
|
+
const [voiceId, setVoiceId] = useState('')
|
|
106
110
|
const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
|
|
107
111
|
const [heartbeatIntervalSec, setHeartbeatIntervalSec] = useState('') // '' = default (30m)
|
|
108
112
|
const [heartbeatModel, setHeartbeatModel] = useState('')
|
|
@@ -121,6 +125,7 @@ export function AgentSheet() {
|
|
|
121
125
|
const [configCopied, setConfigCopied] = useState(false)
|
|
122
126
|
|
|
123
127
|
const soulFileRef = useRef<HTMLInputElement>(null)
|
|
128
|
+
const [soulLibraryOpen, setSoulLibraryOpen] = useState(false)
|
|
124
129
|
const promptFileRef = useRef<HTMLInputElement>(null)
|
|
125
130
|
const importFileRef = useRef<HTMLInputElement>(null)
|
|
126
131
|
|
|
@@ -157,6 +162,8 @@ export function AgentSheet() {
|
|
|
157
162
|
setName(editing.name)
|
|
158
163
|
setDescription(editing.description)
|
|
159
164
|
setSoul(editing.soul || '')
|
|
165
|
+
setSoulInitial(editing.soul || '')
|
|
166
|
+
setSoulSaveState('idle')
|
|
160
167
|
setSystemPrompt(editing.systemPrompt)
|
|
161
168
|
setProvider(editing.provider)
|
|
162
169
|
setModel(editing.model)
|
|
@@ -178,6 +185,7 @@ export function AgentSheet() {
|
|
|
178
185
|
setProjectId(editing.projectId)
|
|
179
186
|
setAvatarSeed(editing.avatarSeed || crypto.randomUUID().slice(0, 8))
|
|
180
187
|
setThinkingLevel(editing.thinkingLevel || '')
|
|
188
|
+
setVoiceId(editing.elevenLabsVoiceId || '')
|
|
181
189
|
setHeartbeatEnabled(editing.heartbeatEnabled || false)
|
|
182
190
|
setHeartbeatIntervalSec(parseDurationToSec(editing.heartbeatInterval, editing.heartbeatIntervalSec))
|
|
183
191
|
setHeartbeatModel(editing.heartbeatModel || '')
|
|
@@ -185,7 +193,10 @@ export function AgentSheet() {
|
|
|
185
193
|
} else {
|
|
186
194
|
setName('')
|
|
187
195
|
setDescription('')
|
|
188
|
-
|
|
196
|
+
const newSoul = randomSoul()
|
|
197
|
+
setSoul(newSoul)
|
|
198
|
+
setSoulInitial(newSoul)
|
|
199
|
+
setSoulSaveState('idle')
|
|
189
200
|
setSystemPrompt('')
|
|
190
201
|
setProvider('claude-cli')
|
|
191
202
|
setModel('')
|
|
@@ -205,6 +216,7 @@ export function AgentSheet() {
|
|
|
205
216
|
setProjectId(undefined)
|
|
206
217
|
setAvatarSeed('')
|
|
207
218
|
setThinkingLevel('')
|
|
219
|
+
setVoiceId('')
|
|
208
220
|
setHeartbeatEnabled(false)
|
|
209
221
|
setHeartbeatIntervalSec('')
|
|
210
222
|
setHeartbeatModel('')
|
|
@@ -299,6 +311,7 @@ export function AgentSheet() {
|
|
|
299
311
|
projectId: projectId || undefined,
|
|
300
312
|
avatarSeed: avatarSeed.trim() || undefined,
|
|
301
313
|
thinkingLevel: thinkingLevel || undefined,
|
|
314
|
+
elevenLabsVoiceId: voiceId.trim() || null,
|
|
302
315
|
heartbeatEnabled,
|
|
303
316
|
heartbeatInterval: heartbeatIntervalSec ? formatHbDuration(Number(heartbeatIntervalSec)) : null,
|
|
304
317
|
heartbeatIntervalSec: heartbeatIntervalSec ? Number(heartbeatIntervalSec) : null,
|
|
@@ -313,6 +326,9 @@ export function AgentSheet() {
|
|
|
313
326
|
toast.success('Agent created')
|
|
314
327
|
}
|
|
315
328
|
await loadAgents()
|
|
329
|
+
setSoulInitial(soul)
|
|
330
|
+
setSoulSaveState('saved')
|
|
331
|
+
setTimeout(() => setSoulSaveState('idle'), 1500)
|
|
316
332
|
onClose()
|
|
317
333
|
}
|
|
318
334
|
|
|
@@ -419,6 +435,7 @@ export function AgentSheet() {
|
|
|
419
435
|
const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
|
|
420
436
|
|
|
421
437
|
return (
|
|
438
|
+
<>
|
|
422
439
|
<BottomSheet open={open} onClose={onClose} wide>
|
|
423
440
|
<div className="mb-10 flex items-start justify-between">
|
|
424
441
|
<div>
|
|
@@ -577,6 +594,24 @@ export function AgentSheet() {
|
|
|
577
594
|
<p className="text-[11px] text-text-3/70 mt-1.5">Controls reasoning depth. Anthropic models use extended thinking; OpenAI o-series uses reasoning_effort. Others get system prompt guidance.</p>
|
|
578
595
|
</div>
|
|
579
596
|
|
|
597
|
+
{/* ElevenLabs Voice ID */}
|
|
598
|
+
{appSettings.elevenLabsEnabled && (
|
|
599
|
+
<div className="mb-8">
|
|
600
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
601
|
+
ElevenLabs Voice ID <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
602
|
+
</label>
|
|
603
|
+
<input
|
|
604
|
+
type="text"
|
|
605
|
+
value={voiceId}
|
|
606
|
+
onChange={(e) => setVoiceId(e.target.value)}
|
|
607
|
+
placeholder="Leave blank for global default"
|
|
608
|
+
className={inputClass}
|
|
609
|
+
style={{ fontFamily: 'inherit' }}
|
|
610
|
+
/>
|
|
611
|
+
<p className="text-[11px] text-text-3/70 mt-1.5">Override the default voice for this agent. Leave blank to use the global default.</p>
|
|
612
|
+
</div>
|
|
613
|
+
)}
|
|
614
|
+
|
|
580
615
|
{/* Heartbeat Configuration */}
|
|
581
616
|
<div className="mb-8">
|
|
582
617
|
<div className="flex items-center justify-between mb-3">
|
|
@@ -633,8 +668,20 @@ export function AgentSheet() {
|
|
|
633
668
|
|
|
634
669
|
{provider !== 'openclaw' && (
|
|
635
670
|
<div className="mb-8">
|
|
636
|
-
<label className="
|
|
671
|
+
<label className="flex items-center gap-2 font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
637
672
|
Soul / Personality <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
673
|
+
{soul !== soulInitial && soulSaveState === 'idle' && (
|
|
674
|
+
<span className="inline-flex items-center gap-1 normal-case tracking-normal text-[10px] text-amber-400 font-600">
|
|
675
|
+
<span className="w-1.5 h-1.5 rounded-full bg-amber-400" />
|
|
676
|
+
Unsaved
|
|
677
|
+
</span>
|
|
678
|
+
)}
|
|
679
|
+
{soulSaveState === 'saved' && (
|
|
680
|
+
<span className="inline-flex items-center gap-1 normal-case tracking-normal text-[10px] text-emerald-400 font-600">
|
|
681
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg>
|
|
682
|
+
Saved
|
|
683
|
+
</span>
|
|
684
|
+
)}
|
|
638
685
|
</label>
|
|
639
686
|
<div className="flex items-center gap-2 mb-3">
|
|
640
687
|
<p className="text-[12px] text-text-3/60">Define the agent's voice, tone, and personality. Injected before the system prompt.</p>
|
|
@@ -652,6 +699,14 @@ export function AgentSheet() {
|
|
|
652
699
|
</svg>
|
|
653
700
|
Shuffle
|
|
654
701
|
</button>
|
|
702
|
+
<button
|
|
703
|
+
type="button"
|
|
704
|
+
onClick={() => setSoulLibraryOpen(true)}
|
|
705
|
+
className="shrink-0 px-2 py-1 rounded-[8px] border border-accent-bright/20 bg-accent-soft text-[11px] text-accent-bright hover:brightness-110 cursor-pointer transition-colors"
|
|
706
|
+
style={{ fontFamily: 'inherit' }}
|
|
707
|
+
>
|
|
708
|
+
Browse Library
|
|
709
|
+
</button>
|
|
655
710
|
<button onClick={() => soulFileRef.current?.click()} className="shrink-0 px-2 py-1 rounded-[8px] border border-white/[0.08] bg-surface text-[11px] text-text-3 hover:text-text-2 cursor-pointer transition-colors" style={{ fontFamily: 'inherit' }}>Upload .md</button>
|
|
656
711
|
<input ref={soulFileRef} type="file" accept=".md,.txt,.markdown" onChange={handleFileUpload(setSoul)} className="hidden" />
|
|
657
712
|
</div>
|
|
@@ -1372,6 +1427,13 @@ export function AgentSheet() {
|
|
|
1372
1427
|
</button>
|
|
1373
1428
|
</div>
|
|
1374
1429
|
</BottomSheet>
|
|
1430
|
+
|
|
1431
|
+
<SoulLibraryPicker
|
|
1432
|
+
open={soulLibraryOpen}
|
|
1433
|
+
onClose={() => setSoulLibraryOpen(false)}
|
|
1434
|
+
onSelect={(s) => setSoul(s)}
|
|
1435
|
+
/>
|
|
1436
|
+
</>
|
|
1375
1437
|
)
|
|
1376
1438
|
}
|
|
1377
1439
|
|