@swarmclawai/swarmclaw 0.6.0 → 0.6.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 +56 -42
- 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 +16 -35
- package/src/app/api/tts/stream/route.ts +14 -42
- 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 +76 -24
- package/src/components/chat/chat-header.tsx +522 -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 +113 -8
- 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 +84 -17
- 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 +125 -14
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/connector-routing.test.ts +118 -1
- package/src/lib/server/connectors/discord.ts +31 -8
- package/src/lib/server/connectors/manager.ts +594 -16
- package/src/lib/server/connectors/media.ts +5 -0
- package/src/lib/server/connectors/telegram.ts +12 -2
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.ts +28 -2
- package/src/lib/server/elevenlabs.test.ts +60 -0
- package/src/lib/server/elevenlabs.ts +103 -0
- 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 +182 -8
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +583 -63
- 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/file.ts +26 -7
- 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 +118 -8
- package/src/lib/server/stream-agent-chat.ts +39 -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
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadTasks, loadAgents } from '@/lib/server/storage'
|
|
3
|
+
|
|
4
|
+
type Range = '24h' | '7d' | '30d'
|
|
5
|
+
|
|
6
|
+
const RANGE_MS: Record<Range, number> = {
|
|
7
|
+
'24h': 24 * 3600_000,
|
|
8
|
+
'7d': 7 * 86400_000,
|
|
9
|
+
'30d': 30 * 86400_000,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function bucketKey(ts: number, range: Range): string {
|
|
13
|
+
const d = new Date(ts)
|
|
14
|
+
if (range === '24h') return d.toISOString().slice(0, 13) // "2026-03-01T14"
|
|
15
|
+
return d.toISOString().slice(0, 10) // "2026-03-01"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function GET(req: Request) {
|
|
19
|
+
const { searchParams } = new URL(req.url)
|
|
20
|
+
const range = (searchParams.get('range') as Range) || '7d'
|
|
21
|
+
const cutoff = Date.now() - (RANGE_MS[range] || RANGE_MS['7d'])
|
|
22
|
+
|
|
23
|
+
const tasks = loadTasks()
|
|
24
|
+
const agents = loadAgents()
|
|
25
|
+
const all = Object.values(tasks)
|
|
26
|
+
|
|
27
|
+
// --- by-status counts ---
|
|
28
|
+
const byStatus: Record<string, number> = {}
|
|
29
|
+
for (const t of all) {
|
|
30
|
+
byStatus[t.status] = (byStatus[t.status] || 0) + 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// WIP = queued + running
|
|
34
|
+
const wip = (byStatus['queued'] || 0) + (byStatus['running'] || 0)
|
|
35
|
+
|
|
36
|
+
// --- completions in range ---
|
|
37
|
+
const completedInRange = all.filter(
|
|
38
|
+
(t) => t.status === 'completed' && t.completedAt && t.completedAt >= cutoff,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// --- cycle times (queuedAt → completedAt) ---
|
|
42
|
+
const cycleTimes: number[] = []
|
|
43
|
+
for (const t of completedInRange) {
|
|
44
|
+
const start = t.queuedAt || t.createdAt
|
|
45
|
+
const end = t.completedAt!
|
|
46
|
+
if (end > start) cycleTimes.push(end - start)
|
|
47
|
+
}
|
|
48
|
+
cycleTimes.sort((a, b) => a - b)
|
|
49
|
+
|
|
50
|
+
const avgCycleMs = cycleTimes.length
|
|
51
|
+
? Math.round(cycleTimes.reduce((s, v) => s + v, 0) / cycleTimes.length)
|
|
52
|
+
: 0
|
|
53
|
+
const p50CycleMs = cycleTimes.length ? cycleTimes[Math.floor(cycleTimes.length * 0.5)] : 0
|
|
54
|
+
const p90CycleMs = cycleTimes.length ? cycleTimes[Math.floor(cycleTimes.length * 0.9)] : 0
|
|
55
|
+
|
|
56
|
+
// --- velocity (completions per bucket) ---
|
|
57
|
+
const velocityMap: Record<string, number> = {}
|
|
58
|
+
for (const t of completedInRange) {
|
|
59
|
+
const key = bucketKey(t.completedAt!, range)
|
|
60
|
+
velocityMap[key] = (velocityMap[key] || 0) + 1
|
|
61
|
+
}
|
|
62
|
+
const velocity = Object.entries(velocityMap)
|
|
63
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
64
|
+
.map(([bucket, count]) => ({ bucket, count }))
|
|
65
|
+
|
|
66
|
+
// --- by-agent completions ---
|
|
67
|
+
const byAgent: Record<string, { agentName: string; completed: number; failed: number }> = {}
|
|
68
|
+
const recentTasks = all.filter(
|
|
69
|
+
(t) => (t.completedAt && t.completedAt >= cutoff) || (t.status === 'failed' && t.updatedAt >= cutoff),
|
|
70
|
+
)
|
|
71
|
+
for (const t of recentTasks) {
|
|
72
|
+
if (!t.agentId) continue
|
|
73
|
+
if (!byAgent[t.agentId]) {
|
|
74
|
+
const agent = agents[t.agentId]
|
|
75
|
+
byAgent[t.agentId] = { agentName: agent?.name || t.agentId, completed: 0, failed: 0 }
|
|
76
|
+
}
|
|
77
|
+
if (t.status === 'completed') byAgent[t.agentId].completed++
|
|
78
|
+
else if (t.status === 'failed') byAgent[t.agentId].failed++
|
|
79
|
+
}
|
|
80
|
+
const byAgentList = Object.values(byAgent).sort((a, b) => b.completed - a.completed)
|
|
81
|
+
|
|
82
|
+
// --- by-priority counts ---
|
|
83
|
+
const byPriority: Record<string, number> = {}
|
|
84
|
+
for (const t of all) {
|
|
85
|
+
const p = t.priority || 'none'
|
|
86
|
+
byPriority[p] = (byPriority[p] || 0) + 1
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return NextResponse.json({
|
|
90
|
+
range,
|
|
91
|
+
byStatus,
|
|
92
|
+
wip,
|
|
93
|
+
completedCount: completedInRange.length,
|
|
94
|
+
avgCycleMs,
|
|
95
|
+
p50CycleMs,
|
|
96
|
+
p90CycleMs,
|
|
97
|
+
velocity,
|
|
98
|
+
byAgent: byAgentList,
|
|
99
|
+
byPriority,
|
|
100
|
+
})
|
|
101
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
|
-
import { loadTasks, saveTasks, loadSettings, logActivity } from '@/lib/server/storage'
|
|
3
|
+
import { loadTasks, saveTasks, loadSettings, loadAgents, logActivity } from '@/lib/server/storage'
|
|
4
4
|
import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
|
|
5
5
|
import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
|
|
6
6
|
import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
|
|
7
7
|
import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
8
8
|
import { notify } from '@/lib/server/ws-hub'
|
|
9
|
+
import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
|
|
10
|
+
import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
|
|
9
11
|
|
|
10
12
|
export async function GET(req: Request) {
|
|
11
13
|
// Keep completed queue integrity even if daemon is not running.
|
|
@@ -64,12 +66,17 @@ export async function POST(req: Request) {
|
|
|
64
66
|
const retryBackoffSec = Number.isFinite(Number(body.retryBackoffSec))
|
|
65
67
|
? Math.max(1, Math.min(3600, Math.trunc(Number(body.retryBackoffSec))))
|
|
66
68
|
: Math.max(1, Math.min(3600, Math.trunc(Number(settings.taskRetryBackoffSec ?? 30))))
|
|
69
|
+
// Resolve @mentions in description to auto-assign agent
|
|
70
|
+
const resolvedAgentId = body.description
|
|
71
|
+
? resolveTaskAgentFromDescription(body.description, body.agentId || '', loadAgents())
|
|
72
|
+
: (body.agentId || '')
|
|
73
|
+
|
|
67
74
|
tasks[id] = {
|
|
68
75
|
id,
|
|
69
76
|
title: body.title || 'Untitled Task',
|
|
70
77
|
description: body.description || '',
|
|
71
78
|
status: body.status || 'backlog',
|
|
72
|
-
agentId:
|
|
79
|
+
agentId: resolvedAgentId,
|
|
73
80
|
projectId: typeof body.projectId === 'string' && body.projectId ? body.projectId : null,
|
|
74
81
|
goalContract: body.goalContract || null,
|
|
75
82
|
cwd: typeof body.cwd === 'string' ? body.cwd : null,
|
|
@@ -94,6 +101,14 @@ export async function POST(req: Request) {
|
|
|
94
101
|
tags: Array.isArray(body.tags) ? body.tags.filter((s: unknown) => typeof s === 'string') : [],
|
|
95
102
|
dueAt: typeof body.dueAt === 'number' ? body.dueAt : null,
|
|
96
103
|
customFields: body.customFields && typeof body.customFields === 'object' ? body.customFields : undefined,
|
|
104
|
+
priority: ['low', 'medium', 'high', 'critical'].includes(body.priority) ? body.priority : undefined,
|
|
105
|
+
fingerprint: computeTaskFingerprint(body.title || 'Untitled Task', body.agentId || ''),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Dedup: if a non-terminal task with same fingerprint exists, return it
|
|
109
|
+
const dupe = findDuplicateTask(tasks, { fingerprint: tasks[id].fingerprint! })
|
|
110
|
+
if (dupe && dupe.id !== id) {
|
|
111
|
+
return NextResponse.json({ ...dupe, deduplicated: true })
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
if (tasks[id].status === 'completed') {
|
package/src/app/api/tts/route.ts
CHANGED
|
@@ -1,40 +1,21 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
2
|
+
import { explainElevenLabsError, resolveElevenLabsConfig, synthesizeElevenLabsMp3 } from '@/lib/server/elevenlabs'
|
|
3
3
|
|
|
4
4
|
export async function POST(req: Request) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
try {
|
|
6
|
+
const { text, voiceId } = await req.json()
|
|
7
|
+
if (!String(text || '').trim()) {
|
|
8
|
+
return new NextResponse('No text provided', { status: 400 })
|
|
9
|
+
}
|
|
10
|
+
resolveElevenLabsConfig(voiceId)
|
|
11
|
+
const audioBuffer = await synthesizeElevenLabsMp3({ text: String(text || ''), voiceId })
|
|
12
|
+
return new NextResponse(new Uint8Array(audioBuffer), {
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'audio/mpeg',
|
|
15
|
+
'Cache-Control': 'no-cache',
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
} catch (err: unknown) {
|
|
19
|
+
return new NextResponse(explainElevenLabsError(err), { status: 500 })
|
|
11
20
|
}
|
|
12
|
-
|
|
13
|
-
const { text } = await req.json()
|
|
14
|
-
const apiRes = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${ELEVENLABS_VOICE}`, {
|
|
15
|
-
method: 'POST',
|
|
16
|
-
headers: {
|
|
17
|
-
'xi-api-key': ELEVENLABS_KEY,
|
|
18
|
-
'Content-Type': 'application/json',
|
|
19
|
-
'Accept': 'audio/mpeg',
|
|
20
|
-
},
|
|
21
|
-
body: JSON.stringify({
|
|
22
|
-
text,
|
|
23
|
-
model_id: 'eleven_multilingual_v2',
|
|
24
|
-
voice_settings: { stability: 0.5, similarity_boost: 0.75 },
|
|
25
|
-
}),
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
if (!apiRes.ok) {
|
|
29
|
-
const err = await apiRes.text()
|
|
30
|
-
return new NextResponse(err, { status: apiRes.status })
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const audioBuffer = await apiRes.arrayBuffer()
|
|
34
|
-
return new NextResponse(audioBuffer, {
|
|
35
|
-
headers: {
|
|
36
|
-
'Content-Type': 'audio/mpeg',
|
|
37
|
-
'Cache-Control': 'no-cache',
|
|
38
|
-
},
|
|
39
|
-
})
|
|
40
21
|
}
|
|
@@ -1,48 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { explainElevenLabsError, requestElevenLabsMp3Stream } from '@/lib/server/elevenlabs'
|
|
2
2
|
|
|
3
3
|
export async function POST(req: Request) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const { text } = await req.json()
|
|
13
|
-
if (!text?.trim()) {
|
|
14
|
-
return new Response('No text provided', { status: 400 })
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const apiRes = await fetch(
|
|
18
|
-
`https://api.elevenlabs.io/v1/text-to-speech/${ELEVENLABS_VOICE}/stream`,
|
|
19
|
-
{
|
|
20
|
-
method: 'POST',
|
|
4
|
+
try {
|
|
5
|
+
const { text, voiceId } = await req.json()
|
|
6
|
+
if (!String(text || '').trim()) {
|
|
7
|
+
return new Response('No text provided', { status: 400 })
|
|
8
|
+
}
|
|
9
|
+
const apiRes = await requestElevenLabsMp3Stream({ text: String(text || ''), voiceId })
|
|
10
|
+
return new Response(apiRes.body, {
|
|
21
11
|
headers: {
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
12
|
+
'Content-Type': 'audio/mpeg',
|
|
13
|
+
'Transfer-Encoding': 'chunked',
|
|
14
|
+
'Cache-Control': 'no-cache',
|
|
25
15
|
},
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
voice_settings: { stability: 0.5, similarity_boost: 0.75 },
|
|
30
|
-
output_format: 'mp3_22050_32',
|
|
31
|
-
}),
|
|
32
|
-
},
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
if (!apiRes.ok) {
|
|
36
|
-
const err = await apiRes.text()
|
|
37
|
-
return new Response(err, { status: apiRes.status })
|
|
16
|
+
})
|
|
17
|
+
} catch (err: unknown) {
|
|
18
|
+
return new Response(explainElevenLabsError(err), { status: 500 })
|
|
38
19
|
}
|
|
39
|
-
|
|
40
|
-
// Pipe the streaming response directly
|
|
41
|
-
return new Response(apiRes.body, {
|
|
42
|
-
headers: {
|
|
43
|
-
'Content-Type': 'audio/mpeg',
|
|
44
|
-
'Transfer-Encoding': 'chunked',
|
|
45
|
-
'Cache-Control': 'no-cache',
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
20
|
}
|
|
@@ -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
|
{(() => {
|