@swarmclawai/swarmclaw 0.5.2 → 0.6.0
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 +42 -7
- package/bin/swarmclaw.js +76 -16
- package/next.config.ts +11 -1
- package/package.json +4 -2
- 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/scripts/postinstall.mjs +18 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
- package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
- package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
- package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
- package/src/app/api/chatrooms/[id]/route.ts +84 -0
- package/src/app/api/chatrooms/route.ts +50 -0
- package/src/app/api/credentials/route.ts +2 -3
- package/src/app/api/knowledge/[id]/route.ts +13 -2
- package/src/app/api/knowledge/route.ts +8 -1
- package/src/app/api/memory/route.ts +8 -0
- package/src/app/api/notifications/[id]/route.ts +27 -0
- package/src/app/api/notifications/route.ts +68 -0
- package/src/app/api/orchestrator/run/route.ts +1 -1
- package/src/app/api/plugins/install/route.ts +2 -2
- package/src/app/api/search/route.ts +155 -0
- package/src/app/api/sessions/[id]/chat/route.ts +2 -0
- package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
- package/src/app/api/sessions/[id]/fork/route.ts +1 -1
- package/src/app/api/sessions/route.ts +3 -3
- package/src/app/api/settings/route.ts +9 -0
- package/src/app/api/setup/check-provider/route.ts +3 -16
- package/src/app/api/skills/[id]/route.ts +6 -0
- package/src/app/api/skills/route.ts +6 -0
- package/src/app/api/tasks/[id]/route.ts +20 -0
- package/src/app/api/tasks/bulk/route.ts +100 -0
- package/src/app/api/tasks/route.ts +1 -0
- package/src/app/api/usage/route.ts +45 -0
- package/src/app/api/webhooks/[id]/route.ts +15 -1
- package/src/app/globals.css +58 -15
- package/src/app/page.tsx +142 -13
- package/src/cli/index.js +42 -0
- package/src/cli/index.test.js +30 -0
- package/src/cli/spec.js +32 -0
- package/src/components/agents/agent-avatar.tsx +57 -10
- package/src/components/agents/agent-card.tsx +48 -15
- package/src/components/agents/agent-chat-list.tsx +123 -10
- package/src/components/agents/agent-list.tsx +50 -19
- package/src/components/agents/agent-sheet.tsx +56 -63
- package/src/components/auth/access-key-gate.tsx +10 -3
- package/src/components/auth/setup-wizard.tsx +2 -2
- package/src/components/auth/user-picker.tsx +31 -3
- package/src/components/chat/activity-moment.tsx +169 -0
- package/src/components/chat/chat-header.tsx +2 -0
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/file-path-chip.tsx +125 -0
- package/src/components/chat/markdown-utils.ts +9 -0
- package/src/components/chat/message-bubble.tsx +46 -295
- package/src/components/chat/message-list.tsx +50 -1
- package/src/components/chat/streaming-bubble.tsx +36 -46
- package/src/components/chat/suggestions-bar.tsx +1 -1
- package/src/components/chat/thinking-indicator.tsx +72 -10
- package/src/components/chat/tool-call-bubble.tsx +66 -70
- package/src/components/chat/tool-request-banner.tsx +31 -7
- package/src/components/chat/transfer-agent-picker.tsx +63 -0
- package/src/components/chatrooms/agent-hover-card.tsx +124 -0
- package/src/components/chatrooms/chatroom-input.tsx +320 -0
- package/src/components/chatrooms/chatroom-list.tsx +123 -0
- package/src/components/chatrooms/chatroom-message.tsx +427 -0
- package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
- package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
- package/src/components/chatrooms/chatroom-view.tsx +344 -0
- package/src/components/chatrooms/reaction-picker.tsx +273 -0
- package/src/components/connectors/connector-sheet.tsx +34 -47
- package/src/components/home/home-view.tsx +501 -0
- package/src/components/input/chat-input.tsx +79 -41
- package/src/components/knowledge/knowledge-list.tsx +31 -1
- package/src/components/knowledge/knowledge-sheet.tsx +83 -2
- package/src/components/layout/app-layout.tsx +209 -83
- package/src/components/layout/mobile-header.tsx +2 -0
- package/src/components/layout/update-banner.tsx +2 -2
- package/src/components/logs/log-list.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
- package/src/components/memory/memory-agent-list.tsx +143 -0
- package/src/components/memory/memory-browser.tsx +205 -0
- package/src/components/memory/memory-card.tsx +34 -7
- package/src/components/memory/memory-detail.tsx +359 -120
- package/src/components/memory/memory-sheet.tsx +157 -23
- package/src/components/plugins/plugin-list.tsx +1 -1
- package/src/components/plugins/plugin-sheet.tsx +1 -1
- package/src/components/projects/project-detail.tsx +509 -0
- package/src/components/projects/project-list.tsx +195 -59
- package/src/components/providers/provider-list.tsx +2 -2
- package/src/components/providers/provider-sheet.tsx +3 -3
- package/src/components/schedules/schedule-card.tsx +3 -2
- package/src/components/schedules/schedule-list.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +25 -25
- package/src/components/secrets/secret-sheet.tsx +47 -24
- package/src/components/secrets/secrets-list.tsx +18 -8
- package/src/components/sessions/new-session-sheet.tsx +33 -65
- package/src/components/sessions/session-card.tsx +45 -14
- package/src/components/sessions/session-list.tsx +35 -18
- package/src/components/shared/agent-picker-list.tsx +90 -0
- package/src/components/shared/agent-switch-dialog.tsx +156 -0
- package/src/components/shared/attachment-chip.tsx +165 -0
- package/src/components/shared/avatar.tsx +10 -1
- package/src/components/shared/check-icon.tsx +12 -0
- package/src/components/shared/confirm-dialog.tsx +1 -1
- package/src/components/shared/empty-state.tsx +32 -0
- package/src/components/shared/file-preview.tsx +34 -0
- package/src/components/shared/form-styles.ts +2 -0
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
- package/src/components/shared/notification-center.tsx +223 -0
- package/src/components/shared/profile-sheet.tsx +115 -0
- package/src/components/shared/reply-quote.tsx +26 -0
- package/src/components/shared/search-dialog.tsx +296 -0
- package/src/components/shared/section-label.tsx +12 -0
- package/src/components/shared/settings/plugin-manager.tsx +1 -1
- package/src/components/shared/settings/section-providers.tsx +1 -1
- package/src/components/shared/settings/section-secrets.tsx +1 -1
- package/src/components/shared/settings/section-theme.tsx +95 -0
- package/src/components/shared/settings/section-user-preferences.tsx +39 -0
- package/src/components/shared/settings/settings-page.tsx +180 -27
- package/src/components/shared/settings/settings-sheet.tsx +9 -73
- package/src/components/shared/sheet-footer.tsx +33 -0
- package/src/components/skills/skill-list.tsx +61 -30
- package/src/components/skills/skill-sheet.tsx +81 -2
- package/src/components/tasks/task-board.tsx +448 -26
- package/src/components/tasks/task-card.tsx +46 -9
- package/src/components/tasks/task-column.tsx +62 -3
- package/src/components/tasks/task-list.tsx +12 -4
- package/src/components/tasks/task-sheet.tsx +89 -72
- package/src/components/ui/hover-card.tsx +52 -0
- package/src/components/usage/metrics-dashboard.tsx +78 -0
- package/src/components/usage/usage-list.tsx +1 -1
- package/src/components/webhooks/webhook-sheet.tsx +1 -1
- package/src/hooks/use-view-router.ts +69 -19
- package/src/instrumentation.ts +15 -1
- package/src/lib/chat.ts +2 -0
- package/src/lib/cron-human.ts +114 -0
- package/src/lib/memory.ts +3 -0
- package/src/lib/server/chat-execution.ts +24 -4
- package/src/lib/server/connectors/manager.ts +11 -0
- package/src/lib/server/context-manager.ts +225 -13
- package/src/lib/server/create-notification.ts +42 -0
- package/src/lib/server/daemon-state.ts +165 -10
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +40 -5
- package/src/lib/server/heartbeat-wake.ts +110 -0
- package/src/lib/server/langgraph-checkpoint.ts +1 -0
- package/src/lib/server/memory-consolidation.ts +92 -0
- package/src/lib/server/memory-db.ts +51 -6
- package/src/lib/server/openclaw-gateway.ts +9 -1
- package/src/lib/server/provider-health.ts +125 -0
- package/src/lib/server/queue.ts +5 -4
- package/src/lib/server/scheduler.ts +8 -0
- package/src/lib/server/session-run-manager.ts +4 -0
- package/src/lib/server/session-tools/chatroom.ts +136 -0
- package/src/lib/server/session-tools/context-mgmt.ts +36 -18
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/memory.ts +6 -1
- package/src/lib/server/storage.ts +80 -29
- package/src/lib/server/stream-agent-chat.ts +153 -47
- package/src/lib/server/system-events.ts +49 -0
- package/src/lib/server/ws-hub.ts +11 -0
- package/src/lib/soul-suggestions.ts +109 -0
- package/src/lib/tasks.ts +4 -1
- package/src/lib/view-routes.ts +36 -1
- package/src/lib/ws-client.ts +14 -4
- package/src/proxy.ts +79 -2
- package/src/stores/use-app-store.ts +94 -3
- package/src/stores/use-chat-store.ts +48 -3
- package/src/stores/use-chatroom-store.ts +276 -0
- package/src/types/index.ts +69 -2
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useMemo, useState } from 'react'
|
|
4
|
-
import ReactMarkdown from 'react-markdown'
|
|
5
|
-
import remarkGfm from 'remark-gfm'
|
|
6
|
-
import rehypeHighlight from 'rehype-highlight'
|
|
7
4
|
import { AiAvatar } from '@/components/shared/avatar'
|
|
8
5
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
9
|
-
import { CodeBlock } from './code-block'
|
|
10
6
|
import { ToolCallBubble } from './tool-call-bubble'
|
|
7
|
+
import { ActivityMoment, isNotableTool } from './activity-moment'
|
|
11
8
|
import { useChatStore, type ToolEvent } from '@/stores/use-chat-store'
|
|
9
|
+
import { isStructuredMarkdown } from './markdown-utils'
|
|
12
10
|
|
|
13
11
|
function ToolEventsSection({ toolEvents }: { toolEvents: ToolEvent[] }) {
|
|
14
12
|
const [expanded, setExpanded] = useState(false)
|
|
@@ -63,18 +61,45 @@ interface Props {
|
|
|
63
61
|
}
|
|
64
62
|
|
|
65
63
|
export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentName }: Props) {
|
|
66
|
-
const rendered = useMemo(() => text, [text])
|
|
67
64
|
const toolEvents = useChatStore((s) => s.toolEvents)
|
|
68
65
|
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
69
66
|
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
67
|
+
const wide = useMemo(() => isStructuredMarkdown(text), [text])
|
|
68
|
+
|
|
69
|
+
// Track which activity moments have been dismissed
|
|
70
|
+
const [dismissedIds, setDismissedIds] = useState<Set<string>>(new Set())
|
|
71
|
+
|
|
72
|
+
// Find the latest completed notable tool event that hasn't been dismissed
|
|
73
|
+
let currentMoment: { id: string; name: string; input: string } | null = null
|
|
74
|
+
for (let i = toolEvents.length - 1; i >= 0; i--) {
|
|
75
|
+
const event = toolEvents[i]
|
|
76
|
+
if (event.status === 'done' && isNotableTool(event.name) && !dismissedIds.has(event.id)) {
|
|
77
|
+
currentMoment = { id: event.id, name: event.name, input: event.input }
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const handleDismiss = (momentId: string) => {
|
|
83
|
+
setDismissedIds((prev) => new Set(prev).add(momentId))
|
|
84
|
+
}
|
|
70
85
|
|
|
71
86
|
return (
|
|
72
87
|
<div
|
|
73
|
-
className="flex flex-col items-start"
|
|
88
|
+
className="flex flex-col items-start relative pl-[44px]"
|
|
74
89
|
style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
75
90
|
>
|
|
91
|
+
<div className="absolute left-[4px] top-0 relative">
|
|
92
|
+
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
93
|
+
{currentMoment && (
|
|
94
|
+
<ActivityMoment
|
|
95
|
+
key={currentMoment.id}
|
|
96
|
+
toolName={currentMoment.name}
|
|
97
|
+
toolInput={currentMoment.input}
|
|
98
|
+
onDismiss={() => handleDismiss(currentMoment!.id)}
|
|
99
|
+
/>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
76
102
|
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
77
|
-
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={36} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
78
103
|
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
79
104
|
<span className="w-2 h-2 rounded-full bg-accent-bright" style={{ animation: 'pulse 1.5s ease infinite' }} />
|
|
80
105
|
{streamPhase === 'tool' && streamToolName && (
|
|
@@ -87,45 +112,10 @@ export function StreamingBubble({ text, assistantName, agentAvatarSeed, agentNam
|
|
|
87
112
|
<ToolEventsSection toolEvents={toolEvents} />
|
|
88
113
|
)}
|
|
89
114
|
|
|
90
|
-
{
|
|
91
|
-
<div className=
|
|
92
|
-
<div className="
|
|
93
|
-
|
|
94
|
-
remarkPlugins={[remarkGfm]}
|
|
95
|
-
rehypePlugins={[rehypeHighlight]}
|
|
96
|
-
components={{
|
|
97
|
-
pre({ children }) {
|
|
98
|
-
return <pre>{children}</pre>
|
|
99
|
-
},
|
|
100
|
-
code({ className, children }) {
|
|
101
|
-
const isBlock = className?.startsWith('language-') || className?.startsWith('hljs')
|
|
102
|
-
if (isBlock) {
|
|
103
|
-
return <CodeBlock className={className}>{children}</CodeBlock>
|
|
104
|
-
}
|
|
105
|
-
return <code className={className}>{children}</code>
|
|
106
|
-
},
|
|
107
|
-
a({ href, children }) {
|
|
108
|
-
if (!href) return <>{children}</>
|
|
109
|
-
const ytMatch = href.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/)
|
|
110
|
-
if (ytMatch) {
|
|
111
|
-
return (
|
|
112
|
-
<div className="my-2">
|
|
113
|
-
<iframe
|
|
114
|
-
src={`https://www.youtube-nocookie.com/embed/${ytMatch[1]}`}
|
|
115
|
-
className="w-full aspect-video rounded-[10px] border border-white/10"
|
|
116
|
-
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
117
|
-
allowFullScreen
|
|
118
|
-
title="YouTube video"
|
|
119
|
-
/>
|
|
120
|
-
</div>
|
|
121
|
-
)
|
|
122
|
-
}
|
|
123
|
-
return <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>
|
|
124
|
-
},
|
|
125
|
-
}}
|
|
126
|
-
>
|
|
127
|
-
{rendered}
|
|
128
|
-
</ReactMarkdown>
|
|
115
|
+
{text && (
|
|
116
|
+
<div className={`${wide ? 'max-w-[92%] md:max-w-[85%]' : 'max-w-[85%] md:max-w-[72%]'} bubble-ai px-5 py-3.5`}>
|
|
117
|
+
<div className="streaming-cursor text-[15px] leading-[1.7] break-words text-text whitespace-pre-wrap">
|
|
118
|
+
{text}
|
|
129
119
|
</div>
|
|
130
120
|
</div>
|
|
131
121
|
)}
|
|
@@ -53,7 +53,7 @@ export function SuggestionsBar({ lastMessage, onSend }: Props) {
|
|
|
53
53
|
|
|
54
54
|
return (
|
|
55
55
|
<div
|
|
56
|
-
className="flex flex-wrap gap-2 px-1 pt-2"
|
|
56
|
+
className="flex flex-wrap gap-2 px-1 pt-2 ml-10"
|
|
57
57
|
style={{ animation: 'fade-in 0.3s cubic-bezier(0.16, 1, 0.3, 1)' }}
|
|
58
58
|
>
|
|
59
59
|
{suggestions.map((text) => (
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import ReactMarkdown from 'react-markdown'
|
|
5
|
+
import remarkGfm from 'remark-gfm'
|
|
3
6
|
import { AiAvatar } from '@/components/shared/avatar'
|
|
4
7
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
5
8
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
@@ -10,31 +13,90 @@ interface Props {
|
|
|
10
13
|
agentName?: string
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
function ElapsedTimer({ startTime }: { startTime: number }) {
|
|
17
|
+
const [elapsed, setElapsed] = useState(0)
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!startTime) return
|
|
21
|
+
const tick = () => setElapsed(Math.floor((Date.now() - startTime) / 1000))
|
|
22
|
+
tick()
|
|
23
|
+
const id = setInterval(tick, 250)
|
|
24
|
+
return () => clearInterval(id)
|
|
25
|
+
}, [startTime])
|
|
26
|
+
|
|
27
|
+
if (!elapsed) return null
|
|
28
|
+
const mins = Math.floor(elapsed / 60)
|
|
29
|
+
const secs = elapsed % 60
|
|
30
|
+
return (
|
|
31
|
+
<span className="text-[10px] text-text-3/50 font-mono tabular-nums">
|
|
32
|
+
{mins > 0 ? `${mins}m ${secs}s` : `${secs}s`}
|
|
33
|
+
</span>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
13
37
|
export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentName }: Props) {
|
|
14
38
|
const streamPhase = useChatStore((s) => s.streamPhase)
|
|
15
39
|
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
40
|
+
const thinkingText = useChatStore((s) => s.thinkingText)
|
|
41
|
+
const thinkingStartTime = useChatStore((s) => s.thinkingStartTime)
|
|
16
42
|
|
|
17
43
|
const statusText = streamPhase === 'tool' && streamToolName
|
|
18
44
|
? `Using ${streamToolName}...`
|
|
19
45
|
: 'Thinking...'
|
|
20
46
|
|
|
47
|
+
const hasThinkingContent = thinkingText.trim().length > 0
|
|
48
|
+
|
|
21
49
|
return (
|
|
22
|
-
<div className="flex flex-col items-start"
|
|
50
|
+
<div className="flex flex-col items-start relative pl-[44px]"
|
|
23
51
|
style={{ animation: 'msg-in-left 0.35s cubic-bezier(0.16, 1, 0.3, 1)' }}>
|
|
52
|
+
<div className="absolute left-[4px] top-0">
|
|
53
|
+
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
54
|
+
</div>
|
|
24
55
|
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
25
|
-
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} name={agentName} size={36} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
26
56
|
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
27
57
|
</div>
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
<
|
|
33
|
-
|
|
58
|
+
|
|
59
|
+
{hasThinkingContent ? (
|
|
60
|
+
<details className="group/think w-full max-w-[85%] md:max-w-[72%]">
|
|
61
|
+
<summary className="bubble-ai px-5 py-3.5 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
|
|
62
|
+
<div className="flex items-center gap-3">
|
|
63
|
+
<div className="flex gap-2">
|
|
64
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
|
|
65
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
|
|
66
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
|
|
67
|
+
</div>
|
|
68
|
+
<span className="text-[12px] text-text-3/60 font-mono">{statusText}</span>
|
|
69
|
+
<ElapsedTimer startTime={thinkingStartTime} />
|
|
70
|
+
<svg
|
|
71
|
+
width="12" height="12" viewBox="0 0 24 24" fill="none"
|
|
72
|
+
stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
|
73
|
+
className="shrink-0 text-text-3/50 transition-transform duration-200 group-open/think:rotate-180 ml-auto"
|
|
74
|
+
>
|
|
75
|
+
<polyline points="6 9 12 15 18 9" />
|
|
76
|
+
</svg>
|
|
77
|
+
</div>
|
|
78
|
+
</summary>
|
|
79
|
+
<div className="mt-2 px-4 py-3 rounded-[12px] bg-bg/60 border border-white/[0.04] max-h-[300px] overflow-y-auto">
|
|
80
|
+
<div className="msg-content text-[13px] leading-[1.6] text-text-3/80">
|
|
81
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
82
|
+
{thinkingText}
|
|
83
|
+
</ReactMarkdown>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
</details>
|
|
87
|
+
) : (
|
|
88
|
+
<div className="bubble-ai px-6 py-5">
|
|
89
|
+
<div className="flex items-center gap-3">
|
|
90
|
+
<div className="flex gap-2">
|
|
91
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
|
|
92
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
|
|
93
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
|
|
94
|
+
</div>
|
|
95
|
+
<span className="text-[12px] text-text-3/60 font-mono">{statusText}</span>
|
|
96
|
+
<ElapsedTimer startTime={thinkingStartTime} />
|
|
34
97
|
</div>
|
|
35
|
-
<span className="text-[12px] text-text-3/60 font-mono">{statusText}</span>
|
|
36
98
|
</div>
|
|
37
|
-
|
|
99
|
+
)}
|
|
38
100
|
</div>
|
|
39
101
|
)
|
|
40
102
|
}
|
|
@@ -308,7 +308,6 @@ function TimeoutQuickFix({ event }: { event: ToolEvent }) {
|
|
|
308
308
|
}
|
|
309
309
|
|
|
310
310
|
export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
311
|
-
const [expanded, setExpanded] = useState(false)
|
|
312
311
|
const [imgExpanded, setImgExpanded] = useState(false)
|
|
313
312
|
const isError = event.status === 'error'
|
|
314
313
|
const color = isError ? '#F43F5E' : (TOOL_COLORS[event.name] || '#6366F1')
|
|
@@ -367,84 +366,81 @@ export function ToolCallBubble({ event }: { event: ToolEvent }) {
|
|
|
367
366
|
|
|
368
367
|
return (
|
|
369
368
|
<div className="w-full text-left">
|
|
370
|
-
<
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
</span>
|
|
391
|
-
{delegationInfo ? (
|
|
392
|
-
<span className="text-[12px] text-text-2 font-mono truncate flex-1">
|
|
393
|
-
<span
|
|
394
|
-
role="link"
|
|
395
|
-
tabIndex={0}
|
|
396
|
-
onClick={handleAgentClick}
|
|
397
|
-
onKeyDown={(e) => e.key === 'Enter' && handleAgentClick(e as unknown as React.MouseEvent)}
|
|
398
|
-
className="text-accent-bright hover:underline cursor-pointer font-600"
|
|
399
|
-
>
|
|
400
|
-
{delegationInfo.agentName}
|
|
401
|
-
</span>
|
|
402
|
-
{delegationInfo.task && <span className="text-text-3">: {delegationInfo.task.slice(0, 80)}</span>}
|
|
403
|
-
</span>
|
|
404
|
-
) : (
|
|
405
|
-
<span className="text-[12px] text-text-2 font-mono truncate flex-1">
|
|
406
|
-
{inputPreview}
|
|
407
|
-
</span>
|
|
408
|
-
)}
|
|
409
|
-
{hasMedia && !expanded && (
|
|
410
|
-
<span className="text-[10px] text-text-3/50 font-500 shrink-0">
|
|
411
|
-
{media.images.length > 0 && `${media.images.length} image${media.images.length > 1 ? 's' : ''}`}
|
|
412
|
-
{media.videos.length > 0 && `${(media.images.length > 0) ? ' · ' : ''}${media.videos.length} video${media.videos.length > 1 ? 's' : ''}`}
|
|
413
|
-
{media.pdfs.length > 0 && `${(media.images.length > 0 || media.videos.length > 0) ? ' · ' : ''}${media.pdfs.length} PDF${media.pdfs.length > 1 ? 's' : ''}`}
|
|
414
|
-
{media.files.length > 0 && `${(media.images.length > 0 || media.videos.length > 0 || media.pdfs.length > 0) ? ' · ' : ''}${media.files.length} file${media.files.length > 1 ? 's' : ''}`}
|
|
369
|
+
<details open={isError || isRunning || undefined} className="group/tool">
|
|
370
|
+
<summary
|
|
371
|
+
className="w-full text-left rounded-[12px] border bg-surface/80 backdrop-blur-sm transition-all duration-200 hover:bg-surface-2 cursor-pointer list-none [&::-webkit-details-marker]:hidden"
|
|
372
|
+
style={{ borderLeft: `3px solid ${color}`, borderColor: `${color}33` }}
|
|
373
|
+
>
|
|
374
|
+
<div className="flex items-center gap-2.5 px-3.5 py-2.5">
|
|
375
|
+
{isRunning ? (
|
|
376
|
+
<span className="w-3.5 h-3.5 shrink-0 rounded-full border-2 border-current animate-spin" style={{ color, borderTopColor: 'transparent' }} />
|
|
377
|
+
) : isError ? (
|
|
378
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round" className="shrink-0">
|
|
379
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
380
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
381
|
+
</svg>
|
|
382
|
+
) : (
|
|
383
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2.5" strokeLinecap="round" className="shrink-0">
|
|
384
|
+
<polyline points="20 6 9 17 4 12" />
|
|
385
|
+
</svg>
|
|
386
|
+
)}
|
|
387
|
+
<span className="label-mono shrink-0" style={{ color }}>
|
|
388
|
+
{label}
|
|
415
389
|
</span>
|
|
416
|
-
|
|
417
|
-
|
|
390
|
+
{delegationInfo ? (
|
|
391
|
+
<span className="text-[12px] text-text-2 font-mono truncate flex-1">
|
|
392
|
+
<span
|
|
393
|
+
role="link"
|
|
394
|
+
tabIndex={0}
|
|
395
|
+
onClick={handleAgentClick}
|
|
396
|
+
onKeyDown={(e) => e.key === 'Enter' && handleAgentClick(e as unknown as React.MouseEvent)}
|
|
397
|
+
className="text-accent-bright hover:underline cursor-pointer font-600"
|
|
398
|
+
>
|
|
399
|
+
{delegationInfo.agentName}
|
|
400
|
+
</span>
|
|
401
|
+
{delegationInfo.task && <span className="text-text-3">: {delegationInfo.task.slice(0, 80)}</span>}
|
|
402
|
+
</span>
|
|
403
|
+
) : (
|
|
404
|
+
<span className="text-[12px] text-text-2 font-mono truncate flex-1">
|
|
405
|
+
{inputPreview}
|
|
406
|
+
</span>
|
|
407
|
+
)}
|
|
408
|
+
{hasMedia && (
|
|
409
|
+
<span className="text-[10px] text-text-3/50 font-500 shrink-0 group-open/tool:hidden">
|
|
410
|
+
{media.images.length > 0 && `${media.images.length} image${media.images.length > 1 ? 's' : ''}`}
|
|
411
|
+
{media.videos.length > 0 && `${(media.images.length > 0) ? ' · ' : ''}${media.videos.length} video${media.videos.length > 1 ? 's' : ''}`}
|
|
412
|
+
{media.pdfs.length > 0 && `${(media.images.length > 0 || media.videos.length > 0) ? ' · ' : ''}${media.pdfs.length} PDF${media.pdfs.length > 1 ? 's' : ''}`}
|
|
413
|
+
{media.files.length > 0 && `${(media.images.length > 0 || media.videos.length > 0 || media.pdfs.length > 0) ? ' · ' : ''}${media.files.length} file${media.files.length > 1 ? 's' : ''}`}
|
|
414
|
+
</span>
|
|
415
|
+
)}
|
|
418
416
|
<svg
|
|
419
417
|
width="12" height="12" viewBox="0 0 24 24" fill="none"
|
|
420
418
|
stroke="currentColor" strokeWidth="2" strokeLinecap="round"
|
|
421
|
-
className=
|
|
419
|
+
className="shrink-0 text-text-3/70 transition-transform duration-200 group-open/tool:rotate-180"
|
|
422
420
|
>
|
|
423
421
|
<polyline points="6 9 12 15 18 9" />
|
|
424
422
|
</svg>
|
|
423
|
+
</div>
|
|
424
|
+
</summary>
|
|
425
|
+
|
|
426
|
+
<div className="px-3.5 pb-3 pt-1 space-y-2 border-t border-white/[0.04] mt-0" onClick={(e) => e.stopPropagation()}>
|
|
427
|
+
<div className="label-mono">Input</div>
|
|
428
|
+
<pre className="text-[12px] text-text-2 font-mono whitespace-pre-wrap break-all bg-bg/50 rounded-[8px] px-3 py-2 max-h-[200px] overflow-y-auto">
|
|
429
|
+
{formattedInput}
|
|
430
|
+
</pre>
|
|
431
|
+
{event.output && (
|
|
432
|
+
<>
|
|
433
|
+
<div className="label-mono mt-2">{isError ? 'Error' : 'Output'}</div>
|
|
434
|
+
{formattedCleanOutput && (
|
|
435
|
+
<pre className="text-[12px] text-text-2 font-mono whitespace-pre-wrap break-all bg-bg/50 rounded-[8px] px-3 py-2 max-h-[300px] overflow-y-auto">
|
|
436
|
+
{formattedCleanOutput}
|
|
437
|
+
</pre>
|
|
438
|
+
)}
|
|
439
|
+
{isError && <TimeoutQuickFix event={event} />}
|
|
440
|
+
</>
|
|
425
441
|
)}
|
|
426
442
|
</div>
|
|
427
|
-
|
|
428
|
-
{expanded && isError && (
|
|
429
|
-
<div className="px-3.5 pb-3 space-y-2" onClick={(e) => e.stopPropagation()}>
|
|
430
|
-
<div className="text-[11px] text-text-3/60 uppercase tracking-wider font-600">Input</div>
|
|
431
|
-
<pre className="text-[12px] text-text-2 font-mono whitespace-pre-wrap break-all bg-bg/50 rounded-[8px] px-3 py-2 max-h-[200px] overflow-y-auto">
|
|
432
|
-
{formattedInput}
|
|
433
|
-
</pre>
|
|
434
|
-
{event.output && (
|
|
435
|
-
<>
|
|
436
|
-
<div className="text-[11px] text-text-3/60 uppercase tracking-wider font-600 mt-2">Error</div>
|
|
437
|
-
{formattedCleanOutput && (
|
|
438
|
-
<pre className="text-[12px] text-text-2 font-mono whitespace-pre-wrap break-all bg-bg/50 rounded-[8px] px-3 py-2 max-h-[300px] overflow-y-auto">
|
|
439
|
-
{formattedCleanOutput}
|
|
440
|
-
</pre>
|
|
441
|
-
)}
|
|
442
|
-
<TimeoutQuickFix event={event} />
|
|
443
|
-
</>
|
|
444
|
-
)}
|
|
445
|
-
</div>
|
|
446
|
-
)}
|
|
447
|
-
</button>
|
|
443
|
+
</details>
|
|
448
444
|
|
|
449
445
|
{/* Render images below the tool call bubble (always visible when present) */}
|
|
450
446
|
{media.images.length > 0 && (
|
|
@@ -16,6 +16,7 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
16
16
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
17
17
|
const sessions = useAppStore((s) => s.sessions)
|
|
18
18
|
const [granted, setGranted] = useState<Set<string>>(new Set())
|
|
19
|
+
const [denied, setDenied] = useState<Set<string>>(new Set())
|
|
19
20
|
const continueSentRef = useRef(false)
|
|
20
21
|
|
|
21
22
|
const toolRequests: { toolId: string; reason: string }[] = []
|
|
@@ -74,10 +75,22 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
const handleDeny = (toolId: string) => {
|
|
79
|
+
setDenied((prev) => new Set(prev).add(toolId))
|
|
80
|
+
const label = TOOL_LABELS[toolId] || toolId
|
|
81
|
+
setTimeout(() => {
|
|
82
|
+
const { streaming, sendMessage } = useChatStore.getState()
|
|
83
|
+
if (!streaming) {
|
|
84
|
+
sendMessage(`Tool access denied for ${label} — proceed without it.`)
|
|
85
|
+
}
|
|
86
|
+
}, 200)
|
|
87
|
+
}
|
|
88
|
+
|
|
77
89
|
return (
|
|
78
90
|
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mt-2">
|
|
79
91
|
{toolRequests.map(({ toolId, reason }) => {
|
|
80
92
|
const isGranted = granted.has(toolId) || (session?.tools || []).includes(toolId)
|
|
93
|
+
const isDenied = denied.has(toolId)
|
|
81
94
|
const label = TOOL_LABELS[toolId] || toolId
|
|
82
95
|
return (
|
|
83
96
|
<div
|
|
@@ -96,14 +109,25 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
96
109
|
</div>
|
|
97
110
|
{isGranted ? (
|
|
98
111
|
<span className="text-[11px] text-emerald-400 font-600 shrink-0">Granted</span>
|
|
112
|
+
) : isDenied ? (
|
|
113
|
+
<span className="text-[11px] text-red-400 font-600 shrink-0">Denied</span>
|
|
99
114
|
) : (
|
|
100
|
-
<
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
115
|
+
<div className="flex gap-1.5 shrink-0">
|
|
116
|
+
<button
|
|
117
|
+
onClick={() => handleGrant(toolId)}
|
|
118
|
+
className="px-3 py-1.5 rounded-[8px] bg-amber-500/20 hover:bg-amber-500/30 text-amber-300 text-[11px] font-600 border-none cursor-pointer transition-colors"
|
|
119
|
+
style={{ fontFamily: 'inherit' }}
|
|
120
|
+
>
|
|
121
|
+
Grant
|
|
122
|
+
</button>
|
|
123
|
+
<button
|
|
124
|
+
onClick={() => handleDeny(toolId)}
|
|
125
|
+
className="px-3 py-1.5 rounded-[8px] bg-red-500/15 hover:bg-red-500/25 text-red-400 text-[11px] font-600 border-none cursor-pointer transition-colors"
|
|
126
|
+
style={{ fontFamily: 'inherit' }}
|
|
127
|
+
>
|
|
128
|
+
Deny
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
107
131
|
)}
|
|
108
132
|
</div>
|
|
109
133
|
)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
6
|
+
|
|
7
|
+
interface TransferAgentPickerProps {
|
|
8
|
+
/** Agent IDs to exclude from the list (e.g. current agent) */
|
|
9
|
+
excludeIds?: string[]
|
|
10
|
+
/** Restrict to these agent IDs only (e.g. chatroom members) */
|
|
11
|
+
filterIds?: string[]
|
|
12
|
+
onSelect: (agentId: string) => void
|
|
13
|
+
onClose: () => void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TransferAgentPicker({ excludeIds, filterIds, onSelect, onClose }: TransferAgentPickerProps) {
|
|
17
|
+
const agents = useAppStore((s) => s.agents)
|
|
18
|
+
const [query, setQuery] = useState('')
|
|
19
|
+
|
|
20
|
+
const excludeSet = new Set(excludeIds || [])
|
|
21
|
+
const filterSet = filterIds ? new Set(filterIds) : null
|
|
22
|
+
|
|
23
|
+
const filtered = Object.values(agents).filter((a) =>
|
|
24
|
+
!a.trashedAt
|
|
25
|
+
&& !excludeSet.has(a.id)
|
|
26
|
+
&& (!filterSet || filterSet.has(a.id))
|
|
27
|
+
&& (!query || a.name.toLowerCase().includes(query.toLowerCase())),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<div className="fixed inset-0 z-40" onClick={onClose} />
|
|
33
|
+
<div className="absolute left-0 bottom-full mb-2 z-50 w-[220px] rounded-[10px] bg-[#1a1a2e]/95 backdrop-blur-xl border border-white/[0.1] shadow-[0_12px_40px_rgba(0,0,0,0.5)] overflow-hidden">
|
|
34
|
+
<div className="p-2">
|
|
35
|
+
<input
|
|
36
|
+
value={query}
|
|
37
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
38
|
+
placeholder="Search agents..."
|
|
39
|
+
autoFocus
|
|
40
|
+
className="w-full px-2 py-1.5 text-[12px] bg-white/[0.06] rounded-[6px] border border-white/[0.08] text-text placeholder:text-text-3/50 outline-none"
|
|
41
|
+
style={{ fontFamily: 'inherit' }}
|
|
42
|
+
/>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="max-h-[200px] overflow-y-auto">
|
|
45
|
+
{filtered.length === 0 && (
|
|
46
|
+
<div className="px-3 py-2 text-[11px] text-text-3/60 text-center">No agents</div>
|
|
47
|
+
)}
|
|
48
|
+
{filtered.map((a) => (
|
|
49
|
+
<button
|
|
50
|
+
key={a.id}
|
|
51
|
+
onClick={() => onSelect(a.id)}
|
|
52
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none"
|
|
53
|
+
style={{ fontFamily: 'inherit' }}
|
|
54
|
+
>
|
|
55
|
+
<AgentAvatar seed={a.avatarSeed} name={a.name} size={20} />
|
|
56
|
+
<span className="text-[12px] text-text truncate">{a.name}</span>
|
|
57
|
+
</button>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { HoverCard, HoverCardTrigger, HoverCardContent } from '@/components/ui/hover-card'
|
|
5
|
+
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
6
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
7
|
+
import { api } from '@/lib/api-client'
|
|
8
|
+
import { AVAILABLE_TOOLS, PLATFORM_TOOLS, TOOL_LABELS } from '@/lib/tool-definitions'
|
|
9
|
+
import type { Agent } from '@/types'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
agent: Agent
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
status?: 'idle' | 'busy' | 'online'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ALL_TOOL_IDS = [...AVAILABLE_TOOLS, ...PLATFORM_TOOLS].map((t) => t.id)
|
|
18
|
+
|
|
19
|
+
export function AgentHoverCard({ agent, children, status }: Props) {
|
|
20
|
+
const [showAll, setShowAll] = useState(false)
|
|
21
|
+
const [busy, setBusy] = useState(false)
|
|
22
|
+
const tools = agent.tools ?? []
|
|
23
|
+
|
|
24
|
+
const displayTools = showAll ? ALL_TOOL_IDS : tools
|
|
25
|
+
|
|
26
|
+
const toggleTool = async (toolId: string) => {
|
|
27
|
+
if (busy) return
|
|
28
|
+
setBusy(true)
|
|
29
|
+
try {
|
|
30
|
+
const current = agent.tools || []
|
|
31
|
+
const updated = current.includes(toolId)
|
|
32
|
+
? current.filter((t) => t !== toolId)
|
|
33
|
+
: [...current, toolId]
|
|
34
|
+
await api('PUT', `/agents/${agent.id}`, { tools: updated })
|
|
35
|
+
useAppStore.getState().loadAgents()
|
|
36
|
+
} finally {
|
|
37
|
+
setBusy(false)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<HoverCard>
|
|
43
|
+
<HoverCardTrigger asChild>
|
|
44
|
+
{children}
|
|
45
|
+
</HoverCardTrigger>
|
|
46
|
+
<HoverCardContent align="start" className="w-[280px]">
|
|
47
|
+
{/* Header: avatar + name + model */}
|
|
48
|
+
<div className="flex items-center gap-2">
|
|
49
|
+
<AgentAvatar seed={agent.avatarSeed || null} name={agent.name} size={28} status={status} />
|
|
50
|
+
<div className="min-w-0 flex-1">
|
|
51
|
+
<div className="text-[13px] font-600 text-text truncate">{agent.name}</div>
|
|
52
|
+
<div className="label-mono truncate">{agent.model}</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Description */}
|
|
57
|
+
{agent.description && (
|
|
58
|
+
<p className="text-[12px] text-text-3 mt-1.5 line-clamp-1">{agent.description}</p>
|
|
59
|
+
)}
|
|
60
|
+
|
|
61
|
+
{/* Tools toggles */}
|
|
62
|
+
<div className="mt-2">
|
|
63
|
+
<div className="flex items-center justify-between mb-1.5">
|
|
64
|
+
<span className="text-[10px] font-600 text-text-3/60 uppercase tracking-wider">Tools</span>
|
|
65
|
+
<button
|
|
66
|
+
onClick={() => setShowAll(!showAll)}
|
|
67
|
+
className="text-[10px] text-accent-bright/70 hover:text-accent-bright font-500 bg-transparent border-none cursor-pointer"
|
|
68
|
+
>
|
|
69
|
+
{showAll ? 'Show enabled' : 'Show all'}
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="max-h-[200px] overflow-y-auto -mx-1 px-1">
|
|
73
|
+
{displayTools.length === 0 && (
|
|
74
|
+
<p className="text-[11px] text-text-3/50 py-1">No tools enabled</p>
|
|
75
|
+
)}
|
|
76
|
+
{displayTools.map((toolId) => {
|
|
77
|
+
const enabled = tools.includes(toolId)
|
|
78
|
+
return (
|
|
79
|
+
<label key={toolId} className="flex items-center gap-2 py-1 cursor-pointer">
|
|
80
|
+
<div
|
|
81
|
+
onClick={(e) => { e.preventDefault(); toggleTool(toolId) }}
|
|
82
|
+
className={`w-7 h-[16px] rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
83
|
+
${enabled ? 'bg-accent-bright' : 'bg-white/[0.12]'}`}
|
|
84
|
+
>
|
|
85
|
+
<div className={`absolute top-[2px] w-[12px] h-[12px] rounded-full bg-white transition-all duration-200
|
|
86
|
+
${enabled ? 'left-[13px]' : 'left-[2px]'}`} />
|
|
87
|
+
</div>
|
|
88
|
+
<span className={`text-[11px] ${enabled ? 'text-text-2' : 'text-text-3/70'}`}>
|
|
89
|
+
{TOOL_LABELS[toolId] || toolId}
|
|
90
|
+
</span>
|
|
91
|
+
</label>
|
|
92
|
+
)
|
|
93
|
+
})}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* Divider */}
|
|
98
|
+
<div className="border-t border-white/[0.06] my-2" />
|
|
99
|
+
|
|
100
|
+
{/* Actions */}
|
|
101
|
+
<div className="flex gap-2">
|
|
102
|
+
<button
|
|
103
|
+
onClick={() => {
|
|
104
|
+
useAppStore.getState().setActiveView('agents')
|
|
105
|
+
useAppStore.getState().setCurrentAgent(agent.id)
|
|
106
|
+
}}
|
|
107
|
+
className="flex-1 text-[12px] font-500 text-text-2 hover:text-text py-1 rounded-[6px] bg-white/[0.04] hover:bg-white/[0.08] transition-colors cursor-pointer"
|
|
108
|
+
>
|
|
109
|
+
Chat
|
|
110
|
+
</button>
|
|
111
|
+
<button
|
|
112
|
+
onClick={() => {
|
|
113
|
+
useAppStore.getState().setEditingAgentId(agent.id)
|
|
114
|
+
useAppStore.getState().setAgentSheetOpen(true)
|
|
115
|
+
}}
|
|
116
|
+
className="flex-1 text-[12px] font-500 text-text-2 hover:text-text py-1 rounded-[6px] bg-white/[0.04] hover:bg-white/[0.08] transition-colors cursor-pointer"
|
|
117
|
+
>
|
|
118
|
+
Edit
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
</HoverCardContent>
|
|
122
|
+
</HoverCard>
|
|
123
|
+
)
|
|
124
|
+
}
|