@swarmclawai/swarmclaw 0.5.0 → 0.5.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/LICENSE +21 -0
- package/README.md +2 -2
- package/package.json +2 -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/activity/route.ts +30 -0
- package/src/app/api/agents/[id]/route.ts +3 -1
- package/src/app/api/agents/route.ts +2 -1
- package/src/app/api/connectors/[id]/route.ts +4 -1
- package/src/app/api/openclaw/approvals/route.ts +20 -0
- package/src/app/api/tasks/[id]/route.ts +37 -1
- package/src/app/api/tasks/route.ts +7 -1
- package/src/app/api/usage/route.ts +74 -22
- package/src/app/api/webhooks/[id]/route.ts +62 -22
- package/src/app/globals.css +78 -3
- package/src/cli/index.js +7 -0
- package/src/cli/spec.js +6 -0
- package/src/components/activity/activity-feed.tsx +91 -0
- package/src/components/agents/agent-card.tsx +7 -3
- package/src/components/agents/agent-list.tsx +33 -3
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/chat/code-block.tsx +1 -1
- package/src/components/chat/exec-approval-card.tsx +6 -3
- package/src/components/chat/message-bubble.tsx +11 -1
- package/src/components/chat/message-list.tsx +28 -1
- package/src/components/chat/trace-block.tsx +15 -25
- package/src/components/layout/app-layout.tsx +21 -7
- package/src/components/tasks/task-board.tsx +40 -2
- package/src/components/tasks/task-card.tsx +40 -2
- package/src/components/tasks/task-sheet.tsx +147 -1
- package/src/components/usage/metrics-dashboard.tsx +278 -0
- package/src/hooks/use-page-active.ts +21 -0
- package/src/hooks/use-ws.ts +13 -1
- package/src/lib/fetch-dedup.ts +20 -0
- package/src/lib/optimistic.ts +25 -0
- package/src/lib/server/connectors/manager.ts +18 -0
- package/src/lib/server/daemon-state.ts +205 -20
- package/src/lib/server/queue.ts +16 -0
- package/src/lib/server/storage.ts +34 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/lib/ws-client.ts +2 -1
- package/src/stores/use-app-store.ts +48 -1
- package/src/stores/use-approval-store.ts +21 -7
- package/src/types/index.ts +40 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { useWs } from '@/hooks/use-ws'
|
|
6
|
+
import type { ActivityEntry } from '@/types'
|
|
7
|
+
|
|
8
|
+
const ENTITY_ICONS: Record<string, string> = {
|
|
9
|
+
agent: 'A', task: 'T', connector: 'C', session: 'S', webhook: 'W', schedule: 'R',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ACTION_COLORS: Record<string, string> = {
|
|
13
|
+
created: 'bg-emerald-500/15 text-emerald-400',
|
|
14
|
+
updated: 'bg-blue-500/15 text-blue-400',
|
|
15
|
+
deleted: 'bg-red-500/15 text-red-400',
|
|
16
|
+
started: 'bg-green-500/15 text-green-400',
|
|
17
|
+
stopped: 'bg-gray-500/15 text-gray-400',
|
|
18
|
+
queued: 'bg-amber-500/15 text-amber-400',
|
|
19
|
+
completed: 'bg-emerald-500/15 text-emerald-400',
|
|
20
|
+
failed: 'bg-red-500/15 text-red-400',
|
|
21
|
+
approved: 'bg-green-500/15 text-green-400',
|
|
22
|
+
rejected: 'bg-red-500/15 text-red-400',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function timeAgo(ts: number) {
|
|
26
|
+
const diff = Date.now() - ts
|
|
27
|
+
if (diff < 60_000) return 'just now'
|
|
28
|
+
if (diff < 3600_000) return `${Math.floor(diff / 60_000)}m ago`
|
|
29
|
+
if (diff < 86400_000) return `${Math.floor(diff / 3600_000)}h ago`
|
|
30
|
+
return `${Math.floor(diff / 86400_000)}d ago`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ENTITY_TYPES = ['', 'agent', 'task', 'connector', 'session', 'webhook', 'schedule'] as const
|
|
34
|
+
|
|
35
|
+
export function ActivityFeed() {
|
|
36
|
+
const entries = useAppStore((s) => s.activityEntries)
|
|
37
|
+
const loadActivity = useAppStore((s) => s.loadActivity)
|
|
38
|
+
const [filterType, setFilterType] = useState('')
|
|
39
|
+
|
|
40
|
+
useEffect(() => { loadActivity({ entityType: filterType || undefined, limit: 100 }) }, [filterType])
|
|
41
|
+
useWs('activity', () => loadActivity({ entityType: filterType || undefined, limit: 100 }), 10_000)
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex-1 flex flex-col h-full overflow-hidden">
|
|
45
|
+
<div className="flex items-center justify-between px-8 pt-6 pb-4 shrink-0">
|
|
46
|
+
<div>
|
|
47
|
+
<h1 className="font-display text-[28px] font-800 tracking-[-0.03em]">Activity</h1>
|
|
48
|
+
<p className="text-[13px] text-text-3 mt-1">Audit trail of all entity mutations</p>
|
|
49
|
+
</div>
|
|
50
|
+
<select
|
|
51
|
+
value={filterType}
|
|
52
|
+
onChange={(e) => setFilterType(e.target.value)}
|
|
53
|
+
className="px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03] appearance-none"
|
|
54
|
+
style={{ fontFamily: 'inherit', minWidth: 130 }}
|
|
55
|
+
>
|
|
56
|
+
<option value="">All Types</option>
|
|
57
|
+
{ENTITY_TYPES.filter(Boolean).map((t) => (
|
|
58
|
+
<option key={t} value={t}>{t.charAt(0).toUpperCase() + t.slice(1)}s</option>
|
|
59
|
+
))}
|
|
60
|
+
</select>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="flex-1 overflow-y-auto px-8 pb-6">
|
|
64
|
+
{entries.length === 0 ? (
|
|
65
|
+
<div className="text-center text-text-3 text-[14px] mt-16">No activity yet</div>
|
|
66
|
+
) : (
|
|
67
|
+
<div className="space-y-1">
|
|
68
|
+
{entries.map((entry: ActivityEntry) => (
|
|
69
|
+
<div key={entry.id} className="flex items-start gap-3 py-3 border-b border-white/[0.04]">
|
|
70
|
+
<div className="w-8 h-8 rounded-[8px] bg-surface-2 flex items-center justify-center text-[12px] font-700 text-text-3 shrink-0">
|
|
71
|
+
{ENTITY_ICONS[entry.entityType] || '?'}
|
|
72
|
+
</div>
|
|
73
|
+
<div className="flex-1 min-w-0">
|
|
74
|
+
<div className="flex items-center gap-2 mb-1">
|
|
75
|
+
<span className={`px-1.5 py-0.5 rounded-[5px] text-[10px] font-600 ${ACTION_COLORS[entry.action] || 'bg-white/[0.06] text-text-3'}`}>
|
|
76
|
+
{entry.action}
|
|
77
|
+
</span>
|
|
78
|
+
<span className="text-[10px] text-text-3/50 font-mono">{entry.entityType}</span>
|
|
79
|
+
<span className="text-[10px] text-text-3/40">{entry.actor}</span>
|
|
80
|
+
</div>
|
|
81
|
+
<p className="text-[13px] text-text-2 leading-[1.4] truncate">{entry.summary}</p>
|
|
82
|
+
</div>
|
|
83
|
+
<span className="text-[11px] text-text-3/50 shrink-0 pt-1">{timeAgo(entry.timestamp)}</span>
|
|
84
|
+
</div>
|
|
85
|
+
))}
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -22,10 +22,11 @@ interface Props {
|
|
|
22
22
|
agent: Agent
|
|
23
23
|
isDefault?: boolean
|
|
24
24
|
isRunning?: boolean
|
|
25
|
+
isSelected?: boolean
|
|
25
26
|
onSetDefault?: (id: string) => void
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
export function AgentCard({ agent, isDefault, isRunning, onSetDefault }: Props) {
|
|
29
|
+
export function AgentCard({ agent, isDefault, isRunning, isSelected, onSetDefault }: Props) {
|
|
29
30
|
const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
|
|
30
31
|
const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
|
|
31
32
|
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
@@ -85,10 +86,13 @@ export function AgentCard({ agent, isDefault, isRunning, onSetDefault }: Props)
|
|
|
85
86
|
<>
|
|
86
87
|
<div
|
|
87
88
|
onClick={handleClick}
|
|
88
|
-
className=
|
|
89
|
+
className={`group relative py-3.5 px-4 cursor-pointer rounded-[14px]
|
|
89
90
|
transition-all duration-200 active:scale-[0.98]
|
|
90
|
-
|
|
91
|
+
${isSelected
|
|
92
|
+
? 'bg-white/[0.04] border border-white/[0.08]'
|
|
93
|
+
: 'bg-transparent border border-transparent hover:bg-white/[0.05] hover:border-white/[0.08]'}`}
|
|
91
94
|
>
|
|
95
|
+
{isSelected && <div className="card-select-indicator" />}
|
|
92
96
|
{/* Three-dot dropdown */}
|
|
93
97
|
<DropdownMenu>
|
|
94
98
|
<DropdownMenuTrigger asChild>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useMemo, useState, useCallback } from 'react'
|
|
3
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState, useCallback } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
6
|
import { AgentCard } from './agent-card'
|
|
@@ -23,10 +23,18 @@ export function AgentList({ inSidebar }: Props) {
|
|
|
23
23
|
const setShowTrash = useAppStore((s) => s.setShowTrash)
|
|
24
24
|
const fleetFilter = useAppStore((s) => s.fleetFilter)
|
|
25
25
|
const setFleetFilter = useAppStore((s) => s.setFleetFilter)
|
|
26
|
+
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
26
27
|
const approvals = useApprovalStore((s) => s.approvals)
|
|
27
28
|
const [search, setSearch] = useState('')
|
|
28
29
|
const [filter, setFilter] = useState<'all' | 'orchestrator' | 'agent'>('all')
|
|
29
30
|
|
|
31
|
+
// FLIP animation refs
|
|
32
|
+
const flipPositions = useRef<Map<string, number>>(new Map())
|
|
33
|
+
const cardRefs = useRef<Map<string, HTMLDivElement>>(new Map())
|
|
34
|
+
|
|
35
|
+
const currentSession = currentSessionId ? sessions[currentSessionId] : null
|
|
36
|
+
const selectedAgentId = currentSession?.agentId
|
|
37
|
+
|
|
30
38
|
const mainSession = useMemo(() =>
|
|
31
39
|
Object.values(sessions).find((s: any) => s.name === '__main__' && s.user === currentUser),
|
|
32
40
|
[sessions, currentUser]
|
|
@@ -76,6 +84,26 @@ export function AgentList({ inSidebar }: Props) {
|
|
|
76
84
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
77
85
|
}, [agents, search, filter, activeProjectFilter, fleetFilter, runningAgentIds, approvalsByAgent])
|
|
78
86
|
|
|
87
|
+
// FLIP animation: animate agent cards when order changes
|
|
88
|
+
useLayoutEffect(() => {
|
|
89
|
+
const newPositions = new Map<string, number>()
|
|
90
|
+
for (const [id, el] of cardRefs.current) {
|
|
91
|
+
const newTop = el.getBoundingClientRect().top
|
|
92
|
+
newPositions.set(id, newTop)
|
|
93
|
+
const prevTop = flipPositions.current.get(id)
|
|
94
|
+
if (prevTop != null) {
|
|
95
|
+
const delta = prevTop - newTop
|
|
96
|
+
if (Math.abs(delta) > 1) {
|
|
97
|
+
el.animate(
|
|
98
|
+
[{ transform: `translateY(${delta}px)` }, { transform: 'translateY(0)' }],
|
|
99
|
+
{ duration: 300, easing: 'cubic-bezier(0.16, 1, 0.3, 1)' }
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
flipPositions.current = newPositions
|
|
105
|
+
}, [filtered])
|
|
106
|
+
|
|
79
107
|
if (showTrash) {
|
|
80
108
|
return (
|
|
81
109
|
<div className="flex-1 flex flex-col overflow-hidden">
|
|
@@ -124,7 +152,7 @@ export function AgentList({ inSidebar }: Props) {
|
|
|
124
152
|
}
|
|
125
153
|
|
|
126
154
|
return (
|
|
127
|
-
<div className="flex-1 overflow-y-auto">
|
|
155
|
+
<div className="flex-1 overflow-y-auto fade-up">
|
|
128
156
|
{(filtered.length > 3 || search) && (
|
|
129
157
|
<div className="px-4 py-2.5">
|
|
130
158
|
<input
|
|
@@ -183,7 +211,9 @@ export function AgentList({ inSidebar }: Props) {
|
|
|
183
211
|
</div>
|
|
184
212
|
<div className="flex flex-col gap-1 px-2 pb-4">
|
|
185
213
|
{filtered.map((p) => (
|
|
186
|
-
<
|
|
214
|
+
<div key={p.id} ref={(el) => { if (el) cardRefs.current.set(p.id, el); else cardRefs.current.delete(p.id) }}>
|
|
215
|
+
<AgentCard agent={p} isDefault={p.id === defaultAgentId} isRunning={runningAgentIds.has(p.id)} isSelected={p.id === selectedAgentId} onSetDefault={handleSetDefault} />
|
|
216
|
+
</div>
|
|
187
217
|
))}
|
|
188
218
|
</div>
|
|
189
219
|
</div>
|
|
@@ -52,7 +52,7 @@ export function InspectorPanel({ agent }: Props) {
|
|
|
52
52
|
const agentSchedules = Object.values(schedules).filter((s) => s.agentId === agent.id)
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
|
-
<div className="w-[400px] shrink-0 border-l border-white/[0.06] bg-[#0d0f1a] flex flex-col h-full overflow-hidden">
|
|
55
|
+
<div className="w-[400px] shrink-0 border-l border-white/[0.06] bg-[#0d0f1a] flex flex-col h-full overflow-hidden fade-up-delay">
|
|
56
56
|
{/* Header */}
|
|
57
57
|
<div className="flex items-center justify-between px-4 py-3 border-b border-white/[0.06] shrink-0">
|
|
58
58
|
<h3 className="font-display text-[14px] font-600 text-text truncate">{agent.name}</h3>
|
|
@@ -56,7 +56,7 @@ export function CodeBlock({ children, className }: Props) {
|
|
|
56
56
|
}, [getText, language])
|
|
57
57
|
|
|
58
58
|
return (
|
|
59
|
-
<div className="relative group/code">
|
|
59
|
+
<div className="relative group/code command-surface">
|
|
60
60
|
<div className="flex items-center justify-between px-4 py-2 bg-black/30 border-b border-white/[0.03]">
|
|
61
61
|
<span className="text-[10px] font-600 uppercase tracking-[0.08em] text-text-3 font-mono">{language}</span>
|
|
62
62
|
<div className="flex items-center gap-1">
|
|
@@ -14,8 +14,9 @@ export function ExecApprovalCard({ approval }: Props) {
|
|
|
14
14
|
resolveApproval(approval.id, decision)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
const alreadyResolved = approval.error?.includes('Already resolved') ?? false
|
|
17
18
|
const expired = approval.expiresAtMs < Date.now()
|
|
18
|
-
const disabled = !!approval.resolving || expired
|
|
19
|
+
const disabled = !!approval.resolving || expired || alreadyResolved
|
|
19
20
|
|
|
20
21
|
return (
|
|
21
22
|
<div className="my-2 rounded-[12px] border border-amber-500/20 bg-amber-500/[0.04] p-3.5">
|
|
@@ -47,11 +48,13 @@ export function ExecApprovalCard({ approval }: Props) {
|
|
|
47
48
|
)}
|
|
48
49
|
</div>
|
|
49
50
|
|
|
50
|
-
{approval.error && (
|
|
51
|
+
{approval.error && !alreadyResolved && (
|
|
51
52
|
<p className="text-[12px] text-red-400 mb-2">{approval.error}</p>
|
|
52
53
|
)}
|
|
53
54
|
|
|
54
|
-
{
|
|
55
|
+
{alreadyResolved ? (
|
|
56
|
+
<p className="text-[12px] text-text-3/50 italic">Already resolved by another session</p>
|
|
57
|
+
) : expired ? (
|
|
55
58
|
<p className="text-[12px] text-text-3/50 italic">Approval expired</p>
|
|
56
59
|
) : (
|
|
57
60
|
<div className="flex items-center gap-2">
|
|
@@ -376,6 +376,15 @@ interface Props {
|
|
|
376
376
|
onFork?: (index: number) => void
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
+
function isStructuredMarkdown(text: string): boolean {
|
|
380
|
+
if (!text) return false
|
|
381
|
+
return /```/.test(text)
|
|
382
|
+
|| /^#{1,4}\s/m.test(text)
|
|
383
|
+
|| /^[-*]\s/m.test(text)
|
|
384
|
+
|| /^\d+\.\s/m.test(text)
|
|
385
|
+
|| /\|.*\|.*\|/m.test(text)
|
|
386
|
+
}
|
|
387
|
+
|
|
379
388
|
export const MessageBubble = memo(function MessageBubble({ message, assistantName, agentAvatarSeed, agentName, isLast, onRetry, messageIndex, onToggleBookmark, onEditResend, onFork }: Props) {
|
|
380
389
|
const isUser = message.role === 'user'
|
|
381
390
|
const isHeartbeat = !isUser && (message.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(message.text || ''))
|
|
@@ -388,6 +397,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
388
397
|
const toolEvents = message.toolEvents || []
|
|
389
398
|
const hasToolEvents = !isUser && toolEvents.length > 0
|
|
390
399
|
const visibleToolEvents = toolEventsExpanded ? [...toolEvents].reverse() : toolEvents.slice(-1)
|
|
400
|
+
const isStructured = !isUser && !isHeartbeat && isStructuredMarkdown(message.text)
|
|
391
401
|
|
|
392
402
|
const handleCopy = useCallback(() => {
|
|
393
403
|
navigator.clipboard.writeText(message.text).then(() => {
|
|
@@ -442,7 +452,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
442
452
|
)}
|
|
443
453
|
|
|
444
454
|
{/* Message bubble */}
|
|
445
|
-
<div className={
|
|
455
|
+
<div className={`${isStructured ? 'max-w-[92%] md:max-w-[85%]' : 'max-w-[85%] md:max-w-[72%]'} ${isUser ? 'bubble-user px-5 py-3.5' : isHeartbeat ? 'bubble-ai px-4 py-3' : 'bubble-ai px-5 py-3.5'}`}>
|
|
446
456
|
{renderAttachments(message)}
|
|
447
457
|
|
|
448
458
|
{isHeartbeat ? (
|
|
@@ -5,6 +5,7 @@ import type { Message } from '@/types'
|
|
|
5
5
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
6
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
7
7
|
import { api } from '@/lib/api-client'
|
|
8
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
9
|
import { MessageBubble } from './message-bubble'
|
|
9
10
|
import { StreamingBubble } from './streaming-bubble'
|
|
10
11
|
import { ThinkingIndicator } from './thinking-indicator'
|
|
@@ -12,6 +13,23 @@ import { SuggestionsBar } from './suggestions-bar'
|
|
|
12
13
|
import { ExecApprovalCard } from './exec-approval-card'
|
|
13
14
|
import { useApprovalStore } from '@/stores/use-approval-store'
|
|
14
15
|
|
|
16
|
+
const INTRO_GREETINGS = [
|
|
17
|
+
'What can I help you with?',
|
|
18
|
+
'Ready when you are.',
|
|
19
|
+
"Let's get started.",
|
|
20
|
+
'How can I assist you today?',
|
|
21
|
+
'What are we working on?',
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
function stableHash(str: string): number {
|
|
25
|
+
let hash = 0
|
|
26
|
+
for (let i = 0; i < str.length; i++) {
|
|
27
|
+
hash = ((hash << 5) - hash) + str.charCodeAt(i)
|
|
28
|
+
hash |= 0
|
|
29
|
+
}
|
|
30
|
+
return Math.abs(hash)
|
|
31
|
+
}
|
|
32
|
+
|
|
15
33
|
function dateSeparator(ts: number): string {
|
|
16
34
|
const d = new Date(ts)
|
|
17
35
|
const today = new Date()
|
|
@@ -316,9 +334,18 @@ export function MessageList({ messages, streaming }: Props) {
|
|
|
316
334
|
<div
|
|
317
335
|
ref={scrollRef}
|
|
318
336
|
onScroll={updateScrollState}
|
|
319
|
-
className="h-full overflow-y-auto px-6 md:px-12 lg:px-16 py-6"
|
|
337
|
+
className="h-full overflow-y-auto px-6 md:px-12 lg:px-16 py-6 fade-up"
|
|
320
338
|
>
|
|
321
339
|
<div className="flex flex-col gap-6">
|
|
340
|
+
{filteredMessages.length === 0 && !streaming && (
|
|
341
|
+
<div className="flex flex-col items-center justify-center gap-3 py-20 text-center" style={{ animation: 'fadeUp 0.5s cubic-bezier(0.16, 1, 0.3, 1) both' }}>
|
|
342
|
+
<AgentAvatar seed={agent?.avatarSeed || null} name={agent?.name || 'Agent'} size={48} />
|
|
343
|
+
<span className="font-display text-[16px] font-600 text-text-2">{agent?.name || 'Assistant'}</span>
|
|
344
|
+
<span className="text-[14px] text-text-3/60">
|
|
345
|
+
{INTRO_GREETINGS[stableHash(agent?.id || session?.id || '') % INTRO_GREETINGS.length]}
|
|
346
|
+
</span>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
322
349
|
{filteredMessages.map((msg, i) => {
|
|
323
350
|
// Find original index in the full messages array for API calls
|
|
324
351
|
const originalIndex = messages.indexOf(msg)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState } from 'react'
|
|
4
3
|
import type { ChatTraceBlock } from '@/types'
|
|
5
4
|
|
|
6
5
|
interface Props {
|
|
@@ -8,8 +7,6 @@ interface Props {
|
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
export function TraceBlock({ trace }: Props) {
|
|
11
|
-
const [collapsed, setCollapsed] = useState(trace.collapsed !== false)
|
|
12
|
-
|
|
13
10
|
const bgColor = trace.type === 'thinking'
|
|
14
11
|
? 'bg-purple-500/[0.04] border-purple-500/10'
|
|
15
12
|
: trace.type === 'tool-call'
|
|
@@ -29,32 +26,25 @@ export function TraceBlock({ trace }: Props) {
|
|
|
29
26
|
: '<'
|
|
30
27
|
|
|
31
28
|
return (
|
|
32
|
-
<
|
|
33
|
-
<
|
|
34
|
-
onClick={() => setCollapsed(!collapsed)}
|
|
35
|
-
className={`w-full flex items-center gap-2 px-3 py-1.5 text-left cursor-pointer border-none bg-transparent transition-colors hover:bg-white/[0.02] ${labelColor}`}
|
|
36
|
-
style={{ fontFamily: 'inherit' }}
|
|
37
|
-
>
|
|
38
|
-
<span className="font-mono text-[10px] w-4 shrink-0">{collapsed ? '+' : '-'}</span>
|
|
29
|
+
<details className={`my-1 rounded-[8px] border ${bgColor} overflow-hidden`} open={trace.collapsed === false || undefined}>
|
|
30
|
+
<summary className={`flex items-center gap-2 px-3 py-1.5 cursor-pointer select-none transition-colors hover:bg-white/[0.02] ${labelColor} [&::-webkit-details-marker]:hidden list-none`}>
|
|
39
31
|
<span className="font-mono text-[10px] shrink-0">{icon}</span>
|
|
40
32
|
<span className="text-[11px] font-600 truncate">
|
|
41
33
|
{trace.label || trace.type.replace('-', ' ')}
|
|
42
34
|
</span>
|
|
43
|
-
</
|
|
44
|
-
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
)}
|
|
57
|
-
</div>
|
|
35
|
+
</summary>
|
|
36
|
+
<div className="px-3 pb-2">
|
|
37
|
+
<pre className={`text-[11px] leading-relaxed whitespace-pre-wrap break-words m-0 ${
|
|
38
|
+
trace.type === 'thinking'
|
|
39
|
+
? 'text-text-3/60 italic'
|
|
40
|
+
: 'text-text-3/70 font-mono'
|
|
41
|
+
}`}>
|
|
42
|
+
{trace.content.length > 2000
|
|
43
|
+
? trace.content.slice(0, 2000) + '\n... (truncated)'
|
|
44
|
+
: trace.content}
|
|
45
|
+
</pre>
|
|
46
|
+
</div>
|
|
47
|
+
</details>
|
|
58
48
|
)
|
|
59
49
|
}
|
|
60
50
|
|
|
@@ -35,8 +35,9 @@ import { KnowledgeList } from '@/components/knowledge/knowledge-list'
|
|
|
35
35
|
import { KnowledgeSheet } from '@/components/knowledge/knowledge-sheet'
|
|
36
36
|
import { PluginList } from '@/components/plugins/plugin-list'
|
|
37
37
|
import { PluginSheet } from '@/components/plugins/plugin-sheet'
|
|
38
|
-
import { UsageList } from '@/components/usage/usage-list'
|
|
39
38
|
import { RunList } from '@/components/runs/run-list'
|
|
39
|
+
import { ActivityFeed } from '@/components/activity/activity-feed'
|
|
40
|
+
import { MetricsDashboard } from '@/components/usage/metrics-dashboard'
|
|
40
41
|
import { ProjectList } from '@/components/projects/project-list'
|
|
41
42
|
import { ProjectSheet } from '@/components/projects/project-sheet'
|
|
42
43
|
import { NetworkBanner } from './network-banner'
|
|
@@ -314,6 +315,11 @@ export function AppLayout() {
|
|
|
314
315
|
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
|
315
316
|
</svg>
|
|
316
317
|
</NavItem>
|
|
318
|
+
<NavItem view="activity" label="Activity" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('activity')}>
|
|
319
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
320
|
+
<path d="M12 8v4l3 3" /><circle cx="12" cy="12" r="10" />
|
|
321
|
+
</svg>
|
|
322
|
+
</NavItem>
|
|
317
323
|
<NavItem view="logs" label="Logs" expanded={railExpanded} active={activeView} sidebarOpen={sidebarOpen} onClick={() => handleNavClick('logs')}>
|
|
318
324
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
319
325
|
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><polyline points="14 2 14 8 20 8" /><line x1="16" y1="13" x2="8" y2="13" /><line x1="16" y1="17" x2="8" y2="17" /><polyline points="10 9 9 9 8 9" />
|
|
@@ -463,7 +469,6 @@ export function AppLayout() {
|
|
|
463
469
|
{activeView === 'webhooks' && <WebhookList inSidebar />}
|
|
464
470
|
{activeView === 'mcp_servers' && <McpServerList />}
|
|
465
471
|
{activeView === 'knowledge' && <KnowledgeList />}
|
|
466
|
-
{activeView === 'usage' && <UsageList />}
|
|
467
472
|
{activeView === 'runs' && <RunList />}
|
|
468
473
|
{activeView === 'logs' && <LogList />}
|
|
469
474
|
</div>
|
|
@@ -562,7 +567,6 @@ export function AppLayout() {
|
|
|
562
567
|
{activeView === 'knowledge' && <KnowledgeList />}
|
|
563
568
|
{activeView === 'plugins' && <PluginList inSidebar />}
|
|
564
569
|
{activeView === 'projects' && <ProjectList />}
|
|
565
|
-
{activeView === 'usage' && <UsageList />}
|
|
566
570
|
{activeView === 'runs' && <RunList />}
|
|
567
571
|
{activeView === 'logs' && <LogList />}
|
|
568
572
|
</div>
|
|
@@ -596,6 +600,10 @@ export function AppLayout() {
|
|
|
596
600
|
<TaskBoard />
|
|
597
601
|
) : activeView === 'memory' ? (
|
|
598
602
|
<MemoryDetail />
|
|
603
|
+
) : activeView === 'activity' ? (
|
|
604
|
+
<ActivityFeed />
|
|
605
|
+
) : activeView === 'usage' ? (
|
|
606
|
+
<MetricsDashboard />
|
|
599
607
|
) : activeView === 'settings' ? (
|
|
600
608
|
<SettingsPage />
|
|
601
609
|
) : !sidebarOpen && FULL_WIDTH_VIEWS.has(activeView) ? (
|
|
@@ -604,7 +612,7 @@ export function AppLayout() {
|
|
|
604
612
|
<h2 className="font-display text-[14px] font-600 text-text-2 tracking-[-0.01em] capitalize flex-1">
|
|
605
613
|
{activeView === 'mcp_servers' ? 'MCP Servers' : activeView.replace('_', ' ')}
|
|
606
614
|
</h2>
|
|
607
|
-
{activeView !== '
|
|
615
|
+
{activeView !== 'runs' && activeView !== 'logs' && (
|
|
608
616
|
<button
|
|
609
617
|
onClick={openNewSheet}
|
|
610
618
|
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[8px] text-[11px] font-600 text-accent-bright bg-accent-soft hover:bg-[#6366F1]/15 transition-all cursor-pointer"
|
|
@@ -627,7 +635,6 @@ export function AppLayout() {
|
|
|
627
635
|
{activeView === 'knowledge' && <KnowledgeList />}
|
|
628
636
|
{activeView === 'plugins' && <PluginList />}
|
|
629
637
|
{activeView === 'projects' && <ProjectList />}
|
|
630
|
-
{activeView === 'usage' && <UsageList />}
|
|
631
638
|
{activeView === 'runs' && <RunList />}
|
|
632
639
|
{activeView === 'logs' && <LogList />}
|
|
633
640
|
</div>
|
|
@@ -742,16 +749,17 @@ const VIEW_DESCRIPTIONS: Record<AppView, string> = {
|
|
|
742
749
|
knowledge: 'Shared knowledge base accessible by all agents',
|
|
743
750
|
logs: 'Application logs & error tracking',
|
|
744
751
|
plugins: 'Extend agent capabilities with custom plugins',
|
|
745
|
-
usage: '
|
|
752
|
+
usage: 'Usage metrics, cost tracking & agent performance',
|
|
746
753
|
runs: 'Live run monitoring & history',
|
|
747
754
|
settings: 'Manage providers, API keys & orchestrator engine',
|
|
748
755
|
projects: 'Group agents, tasks & schedules into projects',
|
|
756
|
+
activity: 'Audit trail of all entity mutations',
|
|
749
757
|
}
|
|
750
758
|
|
|
751
759
|
const FULL_WIDTH_VIEWS = new Set<AppView>([
|
|
752
760
|
'schedules', 'secrets', 'providers', 'skills',
|
|
753
761
|
'connectors', 'webhooks', 'mcp_servers', 'knowledge', 'plugins',
|
|
754
|
-
'usage', 'runs', 'logs', 'settings', 'projects',
|
|
762
|
+
'usage', 'runs', 'logs', 'settings', 'projects', 'activity',
|
|
755
763
|
])
|
|
756
764
|
|
|
757
765
|
const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents'>, { icon: string; title: string; description: string; features: string[] }> = {
|
|
@@ -851,6 +859,12 @@ const VIEW_EMPTY_STATES: Record<Exclude<AppView, 'agents'>, { icon: string; titl
|
|
|
851
859
|
description: 'Organize your work into projects. Group agents, tasks, and schedules under a common scope.',
|
|
852
860
|
features: ['Create named projects with color badges', 'Assign agents and tasks to projects', 'Filter sidebar views by project', 'Global view when no filter is active'],
|
|
853
861
|
},
|
|
862
|
+
activity: {
|
|
863
|
+
icon: 'clock',
|
|
864
|
+
title: 'Activity',
|
|
865
|
+
description: 'Audit trail of all entity mutations across the system.',
|
|
866
|
+
features: ['Track agent, task, and connector changes', 'Filter by entity type and action', 'Real-time updates via WebSocket', 'Relative timestamps'],
|
|
867
|
+
},
|
|
854
868
|
}
|
|
855
869
|
|
|
856
870
|
function ViewEmptyState({ view }: { view: AppView }) {
|
|
@@ -18,16 +18,40 @@ export function TaskBoard() {
|
|
|
18
18
|
const agents = useAppStore((s) => s.agents)
|
|
19
19
|
const showArchived = useAppStore((s) => s.showArchivedTasks)
|
|
20
20
|
const setShowArchived = useAppStore((s) => s.setShowArchivedTasks)
|
|
21
|
-
|
|
21
|
+
// URL-based filter state
|
|
22
|
+
const [filterAgentId, setFilterAgentId] = useState<string>(() => {
|
|
23
|
+
if (typeof window === 'undefined') return ''
|
|
24
|
+
return new URLSearchParams(window.location.search).get('agent') || ''
|
|
25
|
+
})
|
|
26
|
+
const [filterTag, setFilterTag] = useState<string>(() => {
|
|
27
|
+
if (typeof window === 'undefined') return ''
|
|
28
|
+
return new URLSearchParams(window.location.search).get('tag') || ''
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Sync filters to URL
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (typeof window === 'undefined') return
|
|
34
|
+
const params = new URLSearchParams()
|
|
35
|
+
if (filterAgentId) params.set('agent', filterAgentId)
|
|
36
|
+
if (filterTag) params.set('tag', filterTag)
|
|
37
|
+
const qs = params.toString()
|
|
38
|
+
const newUrl = `${window.location.pathname}${qs ? `?${qs}` : ''}`
|
|
39
|
+
window.history.replaceState(null, '', newUrl)
|
|
40
|
+
}, [filterAgentId, filterTag])
|
|
22
41
|
|
|
23
42
|
useEffect(() => { loadTasks(); loadAgents() }, [])
|
|
24
43
|
useWs('tasks', loadTasks, 5000)
|
|
25
44
|
|
|
45
|
+
// Collect all unique tags across tasks
|
|
46
|
+
const allTags = Array.from(new Set(Object.values(tasks).flatMap((t) => t.tags || []))).sort()
|
|
47
|
+
|
|
26
48
|
const columns: BoardTaskStatus[] = showArchived ? [...ACTIVE_COLUMNS, 'archived'] : ACTIVE_COLUMNS
|
|
27
49
|
|
|
28
50
|
const tasksByStatus = (status: BoardTaskStatus) =>
|
|
29
51
|
Object.values(tasks)
|
|
30
|
-
.filter((t) => t.status === status
|
|
52
|
+
.filter((t) => t.status === status
|
|
53
|
+
&& (!filterAgentId || t.agentId === filterAgentId)
|
|
54
|
+
&& (!filterTag || (t.tags && t.tags.includes(filterTag))))
|
|
31
55
|
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
32
56
|
|
|
33
57
|
const handleDrop = useCallback(async (taskId: string, newStatus: BoardTaskStatus) => {
|
|
@@ -59,6 +83,20 @@ export function TaskBoard() {
|
|
|
59
83
|
<option key={a.id} value={a.id}>{a.name}</option>
|
|
60
84
|
))}
|
|
61
85
|
</select>
|
|
86
|
+
{allTags.length > 0 && (
|
|
87
|
+
<select
|
|
88
|
+
value={filterTag}
|
|
89
|
+
onChange={(e) => setFilterTag(e.target.value)}
|
|
90
|
+
className="px-3 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
91
|
+
bg-transparent border-white/[0.06] text-text-3 hover:bg-white/[0.03] appearance-none"
|
|
92
|
+
style={{ fontFamily: 'inherit', minWidth: 110 }}
|
|
93
|
+
>
|
|
94
|
+
<option value="">All Tags</option>
|
|
95
|
+
{allTags.map((tag) => (
|
|
96
|
+
<option key={tag} value={tag}>{tag}</option>
|
|
97
|
+
))}
|
|
98
|
+
</select>
|
|
99
|
+
)}
|
|
62
100
|
<button
|
|
63
101
|
onClick={() => setShowArchived(!showArchived)}
|
|
64
102
|
className={`px-4 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
@@ -4,7 +4,7 @@ import { useState, useCallback } from 'react'
|
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
6
|
import { updateTask, archiveTask } from '@/lib/tasks'
|
|
7
|
-
import type { BoardTask
|
|
7
|
+
import type { BoardTask } from '@/types'
|
|
8
8
|
|
|
9
9
|
function timeAgo(ts: number) {
|
|
10
10
|
const diff = Date.now() - ts
|
|
@@ -25,6 +25,14 @@ export function TaskCard({ task }: { task: BoardTask }) {
|
|
|
25
25
|
|
|
26
26
|
const agent = agents[task.agentId]
|
|
27
27
|
|
|
28
|
+
const isBlocked = Array.isArray(task.blockedBy) && task.blockedBy.length > 0
|
|
29
|
+
const isOverdue = task.dueAt && task.dueAt < Date.now() && task.status !== 'completed' && task.status !== 'archived'
|
|
30
|
+
const borderColor = isBlocked ? 'border-l-rose-500'
|
|
31
|
+
: task.pendingApproval ? 'border-l-amber-500'
|
|
32
|
+
: task.status === 'running' ? 'border-l-emerald-500'
|
|
33
|
+
: task.status === 'failed' ? 'border-l-red-500'
|
|
34
|
+
: 'border-l-transparent'
|
|
35
|
+
|
|
28
36
|
const handleQueue = async (e: React.MouseEvent) => {
|
|
29
37
|
e.stopPropagation()
|
|
30
38
|
await updateTask(task.id, { status: 'queued' })
|
|
@@ -64,17 +72,47 @@ export function TaskCard({ task }: { task: BoardTask }) {
|
|
|
64
72
|
setEditingTaskId(task.id)
|
|
65
73
|
setTaskSheetOpen(true)
|
|
66
74
|
}}
|
|
67
|
-
className={`p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 cursor-grab active:cursor-grabbing
|
|
75
|
+
className={`p-4 rounded-[14px] border border-white/[0.06] border-l-[3px] ${borderColor} bg-surface hover:bg-surface-2 cursor-grab active:cursor-grabbing
|
|
68
76
|
transition-all group ${dragging ? 'opacity-40 scale-[0.97]' : ''}`}
|
|
69
77
|
>
|
|
70
78
|
<div className="flex items-start gap-3 mb-3">
|
|
79
|
+
{isBlocked && (
|
|
80
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-rose-400 shrink-0 mt-0.5">
|
|
81
|
+
<title>{`Blocked by ${task.blockedBy?.length} task(s)`}</title>
|
|
82
|
+
<rect x="3" y="11" width="18" height="11" rx="2" /><path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
83
|
+
</svg>
|
|
84
|
+
)}
|
|
71
85
|
<h4 className="flex-1 text-[14px] font-600 text-text leading-[1.4] line-clamp-2">{task.title}</h4>
|
|
86
|
+
{isBlocked && (
|
|
87
|
+
<span className="px-1.5 py-0.5 rounded-[5px] bg-rose-500/10 text-rose-400 text-[10px] font-600 shrink-0">
|
|
88
|
+
{task.blockedBy?.length}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
72
91
|
</div>
|
|
73
92
|
|
|
74
93
|
{task.description && (
|
|
75
94
|
<p className="text-[12px] text-text-3 line-clamp-2 mb-3">{task.description}</p>
|
|
76
95
|
)}
|
|
77
96
|
|
|
97
|
+
{/* Tags */}
|
|
98
|
+
{task.tags && task.tags.length > 0 && (
|
|
99
|
+
<div className="flex flex-wrap gap-1 mb-3">
|
|
100
|
+
{task.tags.map((tag) => (
|
|
101
|
+
<span key={tag} className="px-1.5 py-0.5 rounded-[5px] bg-indigo-500/10 text-indigo-400 text-[10px] font-600">
|
|
102
|
+
{tag}
|
|
103
|
+
</span>
|
|
104
|
+
))}
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
|
|
108
|
+
{/* Due date */}
|
|
109
|
+
{task.dueAt && (
|
|
110
|
+
<p className={`text-[11px] mb-3 font-600 ${isOverdue ? 'text-red-400' : 'text-text-3/60'}`}>
|
|
111
|
+
Due {new Date(task.dueAt).toLocaleDateString([], { month: 'short', day: 'numeric' })}
|
|
112
|
+
{isOverdue && ' (overdue)'}
|
|
113
|
+
</p>
|
|
114
|
+
)}
|
|
115
|
+
|
|
78
116
|
{task.images && task.images.length > 0 && (
|
|
79
117
|
<div className="flex gap-1.5 mb-3 overflow-x-auto">
|
|
80
118
|
{task.images.slice(0, 3).map((url, i) => (
|