@swarmclawai/swarmclaw 0.5.0 → 0.5.1
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 +1 -1
- package/package.json +1 -1
- package/src/app/globals.css +78 -3
- 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/message-bubble.tsx +11 -1
- package/src/components/chat/message-list.tsx +28 -1
- package/src/components/chat/trace-block.tsx +15 -25
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
|
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
The installer resolves the latest stable release tag and installs that version by default.
|
|
84
|
-
To pin a version: `SWARMCLAW_VERSION=v0.5.
|
|
84
|
+
To pin a version: `SWARMCLAW_VERSION=v0.5.1 curl ... | bash`
|
|
85
85
|
|
|
86
86
|
Or run locally from the repo (friendly for non-technical users):
|
|
87
87
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/app/globals.css
CHANGED
|
@@ -109,6 +109,34 @@
|
|
|
109
109
|
--sidebar-accent-foreground: #e2e2ec;
|
|
110
110
|
--sidebar-border: rgba(255,255,255,0.04);
|
|
111
111
|
--sidebar-ring: rgba(99,102,241,0.4);
|
|
112
|
+
|
|
113
|
+
/* ===== Status Badge System ===== */
|
|
114
|
+
--status-idle-bg: rgba(255,255,255,0.04);
|
|
115
|
+
--status-idle-border: rgba(255,255,255,0.06);
|
|
116
|
+
--status-idle-fg: #8e8ea8;
|
|
117
|
+
--status-running-bg: rgba(52,211,153,0.08);
|
|
118
|
+
--status-running-border: rgba(52,211,153,0.15);
|
|
119
|
+
--status-running-fg: #34D399;
|
|
120
|
+
--status-error-bg: rgba(244,63,94,0.08);
|
|
121
|
+
--status-error-border: rgba(244,63,94,0.15);
|
|
122
|
+
--status-error-fg: #F43F5E;
|
|
123
|
+
--status-connecting-bg: rgba(251,191,36,0.08);
|
|
124
|
+
--status-connecting-border: rgba(251,191,36,0.15);
|
|
125
|
+
--status-connecting-fg: #FBBF24;
|
|
126
|
+
--status-connected-bg: rgba(56,189,248,0.08);
|
|
127
|
+
--status-connected-border: rgba(56,189,248,0.15);
|
|
128
|
+
--status-connected-fg: #38BDF8;
|
|
129
|
+
--status-disconnected-bg: var(--status-idle-bg);
|
|
130
|
+
--status-disconnected-border: var(--status-idle-border);
|
|
131
|
+
--status-disconnected-fg: var(--status-idle-fg);
|
|
132
|
+
--status-approval-bg: rgba(251,146,60,0.08);
|
|
133
|
+
--status-approval-border: rgba(251,146,60,0.15);
|
|
134
|
+
--status-approval-fg: #FB923C;
|
|
135
|
+
|
|
136
|
+
/* ===== Command Surface ===== */
|
|
137
|
+
--command-bg: #0a0a14;
|
|
138
|
+
--command-border: rgba(255,255,255,0.06);
|
|
139
|
+
--command-header: rgba(0,0,0,0.3);
|
|
112
140
|
}
|
|
113
141
|
|
|
114
142
|
@layer base {
|
|
@@ -128,10 +156,15 @@ body {
|
|
|
128
156
|
}
|
|
129
157
|
|
|
130
158
|
/* Scrollbar */
|
|
131
|
-
|
|
159
|
+
* { scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.08) transparent; }
|
|
160
|
+
::-webkit-scrollbar { width: 6px; height: 6px; }
|
|
132
161
|
::-webkit-scrollbar-track { background: transparent; }
|
|
133
|
-
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.
|
|
134
|
-
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.
|
|
162
|
+
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 6px; }
|
|
163
|
+
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.15); }
|
|
164
|
+
textarea { scrollbar-width: none; }
|
|
165
|
+
textarea::-webkit-scrollbar { width: 0; }
|
|
166
|
+
textarea:hover { scrollbar-width: thin; }
|
|
167
|
+
textarea:hover::-webkit-scrollbar { width: 6px; }
|
|
135
168
|
|
|
136
169
|
/* Selection */
|
|
137
170
|
::selection { background: rgba(99,102,241,0.3); }
|
|
@@ -205,6 +238,10 @@ body {
|
|
|
205
238
|
50% { background-position: 100% 50%; }
|
|
206
239
|
100% { background-position: 0% 50%; }
|
|
207
240
|
}
|
|
241
|
+
@keyframes fadeUp {
|
|
242
|
+
from { opacity: 0; transform: translateY(10px); }
|
|
243
|
+
to { opacity: 1; transform: translateY(0); }
|
|
244
|
+
}
|
|
208
245
|
|
|
209
246
|
/* AI avatar mood animations */
|
|
210
247
|
@keyframes ai-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.15); } }
|
|
@@ -377,6 +414,44 @@ body {
|
|
|
377
414
|
font-family: var(--font-sora), 'Sora', system-ui, sans-serif;
|
|
378
415
|
}
|
|
379
416
|
|
|
417
|
+
/* ===== Status Badge Classes ===== */
|
|
418
|
+
.status-badge-idle, .status-badge-running, .status-badge-error,
|
|
419
|
+
.status-badge-connecting, .status-badge-connected, .status-badge-disconnected,
|
|
420
|
+
.status-badge-approval {
|
|
421
|
+
display: inline-flex; align-items: center; gap: 0.375rem;
|
|
422
|
+
padding: 0.125rem 0.5rem; border-radius: 6px;
|
|
423
|
+
font-size: 10px; font-weight: 600; text-transform: uppercase;
|
|
424
|
+
letter-spacing: 0.05em; border: 1px solid;
|
|
425
|
+
}
|
|
426
|
+
.status-badge-idle { background: var(--status-idle-bg); border-color: var(--status-idle-border); color: var(--status-idle-fg); }
|
|
427
|
+
.status-badge-running { background: var(--status-running-bg); border-color: var(--status-running-border); color: var(--status-running-fg); }
|
|
428
|
+
.status-badge-error { background: var(--status-error-bg); border-color: var(--status-error-border); color: var(--status-error-fg); }
|
|
429
|
+
.status-badge-connecting { background: var(--status-connecting-bg); border-color: var(--status-connecting-border); color: var(--status-connecting-fg); }
|
|
430
|
+
.status-badge-connected { background: var(--status-connected-bg); border-color: var(--status-connected-border); color: var(--status-connected-fg); }
|
|
431
|
+
.status-badge-disconnected { background: var(--status-disconnected-bg); border-color: var(--status-disconnected-border); color: var(--status-disconnected-fg); }
|
|
432
|
+
.status-badge-approval { background: var(--status-approval-bg); border-color: var(--status-approval-border); color: var(--status-approval-fg); }
|
|
433
|
+
|
|
434
|
+
/* ===== Fade-Up Animation ===== */
|
|
435
|
+
.fade-up { animation: fadeUp 0.35s cubic-bezier(0.16, 1, 0.3, 1) both; }
|
|
436
|
+
.fade-up-delay { animation: fadeUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) 0.08s both; }
|
|
437
|
+
|
|
438
|
+
/* ===== Card Select Indicator ===== */
|
|
439
|
+
.card-select-indicator {
|
|
440
|
+
position: absolute;
|
|
441
|
+
left: 0; top: 0.7rem; bottom: 0.7rem;
|
|
442
|
+
width: 4px;
|
|
443
|
+
border-radius: 0 9999px 9999px 0;
|
|
444
|
+
background: linear-gradient(180deg, #818CF8 0%, #6366F1 50%, #4F46E5 100%);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* ===== Command Surface ===== */
|
|
448
|
+
.command-surface {
|
|
449
|
+
background: var(--command-bg);
|
|
450
|
+
border: 1px solid var(--command-border);
|
|
451
|
+
border-radius: 12px;
|
|
452
|
+
overflow: hidden;
|
|
453
|
+
}
|
|
454
|
+
|
|
380
455
|
@layer base {
|
|
381
456
|
* {
|
|
382
457
|
@apply border-border outline-ring/50;
|
|
@@ -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">
|
|
@@ -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
|
|