@swarmclawai/swarmclaw 0.6.8 → 0.7.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 +70 -45
- package/next.config.ts +31 -6
- package/package.json +3 -2
- package/src/app/api/agents/[id]/thread/route.ts +1 -0
- package/src/app/api/agents/route.ts +18 -5
- package/src/app/api/approvals/route.ts +22 -0
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
- package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
- package/src/app/api/memory/route.ts +36 -5
- package/src/app/api/notifications/route.ts +3 -0
- package/src/app/api/plugins/install/route.ts +57 -5
- package/src/app/api/plugins/marketplace/route.ts +73 -22
- package/src/app/api/plugins/route.ts +61 -1
- package/src/app/api/plugins/ui/route.ts +34 -0
- package/src/app/api/settings/route.ts +62 -0
- package/src/app/api/setup/doctor/route.ts +22 -5
- package/src/app/api/tasks/[id]/approve/route.ts +4 -3
- package/src/app/api/tasks/[id]/route.ts +11 -3
- package/src/app/api/tasks/route.ts +8 -2
- package/src/app/globals.css +27 -0
- package/src/app/page.tsx +10 -5
- package/src/cli/index.js +13 -0
- package/src/components/activity/activity-feed.tsx +9 -2
- package/src/components/agents/agent-avatar.tsx +5 -1
- package/src/components/agents/agent-card.tsx +55 -9
- package/src/components/agents/agent-sheet.tsx +86 -29
- package/src/components/agents/inspector-panel.tsx +1 -1
- package/src/components/auth/access-key-gate.tsx +63 -54
- package/src/components/auth/user-picker.tsx +37 -32
- package/src/components/chat/chat-area.tsx +11 -0
- package/src/components/chat/chat-header.tsx +69 -25
- package/src/components/chat/chat-tool-toggles.tsx +2 -2
- package/src/components/chat/code-block.tsx +3 -1
- package/src/components/chat/exec-approval-card.tsx +8 -1
- package/src/components/chat/message-bubble.tsx +164 -4
- package/src/components/chat/message-list.tsx +30 -4
- package/src/components/chat/session-approval-card.tsx +80 -0
- package/src/components/chat/streaming-bubble.tsx +6 -5
- package/src/components/chat/thinking-indicator.tsx +48 -12
- package/src/components/chat/tool-request-banner.tsx +39 -20
- package/src/components/chatrooms/chatroom-list.tsx +11 -4
- package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
- package/src/components/connectors/connector-list.tsx +33 -11
- package/src/components/connectors/connector-sheet.tsx +29 -6
- package/src/components/home/home-view.tsx +20 -14
- package/src/components/input/chat-input.tsx +22 -1
- package/src/components/knowledge/knowledge-list.tsx +17 -18
- package/src/components/knowledge/knowledge-sheet.tsx +9 -5
- package/src/components/layout/app-layout.tsx +73 -21
- package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
- package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
- package/src/components/memory/memory-list.tsx +20 -13
- package/src/components/plugins/plugin-list.tsx +213 -59
- package/src/components/plugins/plugin-sheet.tsx +119 -24
- package/src/components/projects/project-list.tsx +17 -9
- package/src/components/providers/provider-list.tsx +21 -6
- package/src/components/providers/provider-sheet.tsx +42 -25
- package/src/components/runs/run-list.tsx +17 -13
- package/src/components/schedules/schedule-card.tsx +10 -3
- package/src/components/schedules/schedule-list.tsx +2 -2
- package/src/components/schedules/schedule-sheet.tsx +19 -7
- package/src/components/secrets/secret-sheet.tsx +7 -2
- package/src/components/secrets/secrets-list.tsx +18 -5
- package/src/components/sessions/new-session-sheet.tsx +183 -376
- package/src/components/sessions/session-card.tsx +10 -2
- package/src/components/settings/gateway-connection-panel.tsx +9 -8
- package/src/components/shared/command-palette.tsx +13 -5
- package/src/components/shared/empty-state.tsx +20 -8
- package/src/components/shared/notification-center.tsx +134 -86
- package/src/components/shared/profile-sheet.tsx +4 -0
- package/src/components/shared/settings/plugin-manager.tsx +360 -135
- package/src/components/shared/settings/section-capability-policy.tsx +3 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
- package/src/components/skills/clawhub-browser.tsx +1 -0
- package/src/components/skills/skill-list.tsx +31 -12
- package/src/components/skills/skill-sheet.tsx +20 -7
- package/src/components/tasks/approvals-panel.tsx +170 -66
- package/src/components/tasks/task-board.tsx +20 -12
- package/src/components/tasks/task-card.tsx +21 -7
- package/src/components/tasks/task-column.tsx +4 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +130 -1
- package/src/components/ui/dialog.tsx +1 -0
- package/src/components/ui/sheet.tsx +1 -0
- package/src/components/usage/metrics-dashboard.tsx +66 -64
- package/src/components/wallets/wallet-panel.tsx +65 -41
- package/src/components/wallets/wallet-section.tsx +9 -3
- package/src/components/webhooks/webhook-list.tsx +21 -12
- package/src/components/webhooks/webhook-sheet.tsx +13 -3
- package/src/lib/approval-display.test.ts +45 -0
- package/src/lib/approval-display.ts +62 -0
- package/src/lib/clipboard.ts +38 -0
- package/src/lib/memory.ts +8 -0
- package/src/lib/providers/claude-cli.ts +5 -3
- package/src/lib/providers/index.ts +67 -21
- package/src/lib/runtime-loop.ts +3 -2
- package/src/lib/server/approvals.ts +150 -0
- package/src/lib/server/chat-execution.ts +223 -62
- package/src/lib/server/clawhub-client.ts +82 -6
- package/src/lib/server/connectors/manager.ts +27 -1
- package/src/lib/server/cost.test.ts +73 -0
- package/src/lib/server/cost.ts +165 -34
- package/src/lib/server/daemon-state.ts +42 -0
- package/src/lib/server/data-dir.ts +18 -1
- package/src/lib/server/integrity-monitor.ts +208 -0
- package/src/lib/server/llm-response-cache.test.ts +102 -0
- package/src/lib/server/llm-response-cache.ts +227 -0
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/main-session.ts +6 -3
- package/src/lib/server/mcp-conformance.test.ts +18 -0
- package/src/lib/server/mcp-conformance.ts +233 -0
- package/src/lib/server/memory-db.ts +180 -17
- package/src/lib/server/memory-retrieval.test.ts +56 -0
- package/src/lib/server/orchestrator-lg.ts +4 -1
- package/src/lib/server/orchestrator.ts +4 -3
- package/src/lib/server/plugins.ts +650 -142
- package/src/lib/server/process-manager.ts +18 -0
- package/src/lib/server/queue.ts +253 -11
- package/src/lib/server/runtime-settings.ts +9 -0
- package/src/lib/server/session-run-manager.test.ts +23 -0
- package/src/lib/server/session-run-manager.ts +11 -1
- package/src/lib/server/session-tools/canvas.ts +85 -50
- package/src/lib/server/session-tools/chatroom.ts +130 -127
- package/src/lib/server/session-tools/connector.ts +233 -454
- package/src/lib/server/session-tools/context-mgmt.ts +87 -105
- package/src/lib/server/session-tools/crud.ts +84 -7
- package/src/lib/server/session-tools/delegate.ts +351 -752
- package/src/lib/server/session-tools/discovery.ts +198 -0
- package/src/lib/server/session-tools/edit_file.ts +82 -0
- package/src/lib/server/session-tools/file-send.test.ts +39 -0
- package/src/lib/server/session-tools/file.ts +257 -425
- package/src/lib/server/session-tools/git.ts +87 -47
- package/src/lib/server/session-tools/http.ts +85 -33
- package/src/lib/server/session-tools/index.ts +205 -160
- package/src/lib/server/session-tools/memory.ts +152 -265
- package/src/lib/server/session-tools/monitor.ts +126 -0
- package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
- package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
- package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
- package/src/lib/server/session-tools/platform.ts +86 -0
- package/src/lib/server/session-tools/plugin-creator.ts +239 -0
- package/src/lib/server/session-tools/sample-ui.ts +97 -0
- package/src/lib/server/session-tools/sandbox.ts +175 -148
- package/src/lib/server/session-tools/schedule.ts +66 -31
- package/src/lib/server/session-tools/session-info.ts +104 -410
- package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
- package/src/lib/server/session-tools/shell.ts +171 -143
- package/src/lib/server/session-tools/subagent.ts +77 -77
- package/src/lib/server/session-tools/wallet.ts +182 -106
- package/src/lib/server/session-tools/web.ts +179 -349
- package/src/lib/server/storage.ts +24 -0
- package/src/lib/server/stream-agent-chat.ts +301 -244
- package/src/lib/server/task-quality-gate.test.ts +44 -0
- package/src/lib/server/task-quality-gate.ts +67 -0
- package/src/lib/server/task-validation.test.ts +78 -0
- package/src/lib/server/task-validation.ts +67 -2
- package/src/lib/server/tool-aliases.ts +68 -0
- package/src/lib/server/tool-capability-policy.ts +23 -5
- package/src/lib/tasks.ts +7 -1
- package/src/lib/tool-definitions.ts +23 -23
- package/src/lib/validation/schemas.ts +12 -0
- package/src/lib/view-routes.ts +2 -24
- package/src/stores/use-app-store.ts +23 -1
- package/src/types/index.ts +121 -7
|
@@ -40,31 +40,64 @@ export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentAvatarU
|
|
|
40
40
|
const streamToolName = useChatStore((s) => s.streamToolName)
|
|
41
41
|
const thinkingText = useChatStore((s) => s.thinkingText)
|
|
42
42
|
const thinkingStartTime = useChatStore((s) => s.thinkingStartTime)
|
|
43
|
+
const agentStatus = useChatStore((s) => s.agentStatus)
|
|
43
44
|
|
|
44
45
|
const statusText = streamPhase === 'tool' && streamToolName
|
|
45
46
|
? `Using ${streamToolName}...`
|
|
46
47
|
: 'Thinking...'
|
|
47
48
|
|
|
48
49
|
const hasThinkingContent = thinkingText.trim().length > 0
|
|
50
|
+
const hasMission = !!agentStatus?.goal
|
|
49
51
|
|
|
50
52
|
return (
|
|
51
53
|
<div className="flex flex-col items-start relative pl-[44px]"
|
|
52
|
-
style={{ animation: 'msg-in-left 0.
|
|
54
|
+
style={{ animation: 'msg-in-left 0.4s var(--ease-spring) both' }}>
|
|
53
55
|
<div className="absolute left-[4px] top-0">
|
|
54
56
|
{agentName ? <AgentAvatar seed={agentAvatarSeed || null} avatarUrl={agentAvatarUrl} name={agentName} size={28} /> : <AiAvatar size="sm" mood={streamPhase === 'tool' ? 'tool' : 'thinking'} />}
|
|
55
57
|
</div>
|
|
58
|
+
|
|
56
59
|
<div className="flex items-center gap-2.5 mb-2 px-1">
|
|
57
60
|
<span className="text-[12px] font-600 text-text-3">{assistantName || 'Claude'}</span>
|
|
61
|
+
{agentStatus?.status && (
|
|
62
|
+
<span className={`px-1.5 py-0.5 rounded-[4px] text-[9px] font-700 uppercase tracking-wider ${
|
|
63
|
+
agentStatus.status === 'progress' ? 'bg-blue-500/10 text-blue-400' :
|
|
64
|
+
agentStatus.status === 'ok' ? 'bg-emerald-500/10 text-emerald-400' :
|
|
65
|
+
agentStatus.status === 'blocked' ? 'bg-red-500/10 text-red-400' :
|
|
66
|
+
'bg-white/[0.06] text-text-3'
|
|
67
|
+
}`} style={{ animation: 'spring-in 0.3s var(--ease-spring)' }}>
|
|
68
|
+
{agentStatus.status}
|
|
69
|
+
</span>
|
|
70
|
+
)}
|
|
58
71
|
</div>
|
|
59
72
|
|
|
73
|
+
{hasMission && (
|
|
74
|
+
<div className="mb-2 w-full max-w-[85%] md:max-w-[72%] p-3 rounded-[12px] border border-accent-bright/10 bg-accent-bright/[0.02]"
|
|
75
|
+
style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
|
|
76
|
+
<div className="text-[10px] font-700 text-accent-bright/60 uppercase tracking-widest mb-1.5 flex items-center gap-2">
|
|
77
|
+
<span className="w-1 h-1 rounded-full bg-accent-bright/40" />
|
|
78
|
+
Active Mission
|
|
79
|
+
</div>
|
|
80
|
+
<p className="text-[13px] font-500 text-text-2 leading-snug">{agentStatus.goal}</p>
|
|
81
|
+
{agentStatus.nextAction && (
|
|
82
|
+
<div className="mt-2 pt-2 border-t border-white/[0.04]">
|
|
83
|
+
<span className="text-[10px] font-600 text-text-3/40 uppercase block mb-0.5">Next Action</span>
|
|
84
|
+
<p className="text-[11px] text-text-3/80 italic">“{agentStatus.nextAction}”</p>
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
60
90
|
{hasThinkingContent ? (
|
|
61
91
|
<details className="group/think w-full max-w-[85%] md:max-w-[72%]">
|
|
62
|
-
<summary className="bubble-ai px-5 py-3.5 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
|
|
63
|
-
|
|
92
|
+
<summary className="bubble-ai px-5 py-3.5 cursor-pointer list-none [&::-webkit-details-marker]:hidden relative overflow-hidden group-open/think:rounded-b-none border border-transparent hover:border-white/[0.04] transition-all">
|
|
93
|
+
{/* Thinking pulse background */}
|
|
94
|
+
<div className="absolute inset-0 bg-accent-bright/5 opacity-0 group-hover/think:opacity-100 transition-opacity" style={{ animation: 'pulse-subtle 2s ease-in-out infinite' }} />
|
|
95
|
+
|
|
96
|
+
<div className="flex items-center gap-3 relative z-10">
|
|
64
97
|
<div className="flex gap-2">
|
|
65
|
-
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
|
|
66
|
-
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
|
|
67
|
-
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
|
|
98
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
|
|
99
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
|
|
100
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
|
|
68
101
|
</div>
|
|
69
102
|
<span className="text-[12px] text-text-3/60 font-mono">{statusText}</span>
|
|
70
103
|
<ElapsedTimer startTime={thinkingStartTime} />
|
|
@@ -77,7 +110,7 @@ export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentAvatarU
|
|
|
77
110
|
</svg>
|
|
78
111
|
</div>
|
|
79
112
|
</summary>
|
|
80
|
-
<div className="
|
|
113
|
+
<div className="px-4 py-3 rounded-b-[12px] bg-bg/60 border-x border-b border-white/[0.04] max-h-[300px] overflow-y-auto">
|
|
81
114
|
<div className="msg-content text-[13px] leading-[1.6] text-text-3/80">
|
|
82
115
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
83
116
|
{thinkingText}
|
|
@@ -86,12 +119,15 @@ export function ThinkingIndicator({ assistantName, agentAvatarSeed, agentAvatarU
|
|
|
86
119
|
</div>
|
|
87
120
|
</details>
|
|
88
121
|
) : (
|
|
89
|
-
<div className="bubble-ai px-6 py-5">
|
|
90
|
-
|
|
122
|
+
<div className="bubble-ai px-6 py-5 relative overflow-hidden">
|
|
123
|
+
{/* Thinking glow effect */}
|
|
124
|
+
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-accent-bright/5 to-transparent" style={{ animation: 'shimmer-bar 3s linear infinite' }} />
|
|
125
|
+
|
|
126
|
+
<div className="flex items-center gap-3 relative z-10">
|
|
91
127
|
<div className="flex gap-2">
|
|
92
|
-
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
|
|
93
|
-
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
|
|
94
|
-
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
|
|
128
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite' }} />
|
|
129
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.15s' }} />
|
|
130
|
+
<span className="w-[6px] h-[6px] rounded-full bg-accent-bright/60 shadow-[0_0_8px_rgba(129,140,248,0.4)]" style={{ animation: 'dot-bounce 1.2s ease-in-out infinite 0.3s' }} />
|
|
95
131
|
</div>
|
|
96
132
|
<span className="text-[12px] text-text-3/60 font-mono">{statusText}</span>
|
|
97
133
|
<ElapsedTimer startTime={thinkingStartTime} />
|
|
@@ -15,22 +15,36 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
15
15
|
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
16
16
|
const currentSessionId = useAppStore((s) => s.currentSessionId)
|
|
17
17
|
const sessions = useAppStore((s) => s.sessions)
|
|
18
|
+
const serverApprovals = useAppStore((s) => s.approvals)
|
|
19
|
+
const loadApprovals = useAppStore((s) => s.loadApprovals)
|
|
18
20
|
const [granted, setGranted] = useState<Set<string>>(new Set())
|
|
19
21
|
const [denied, setDenied] = useState<Set<string>>(new Set())
|
|
20
22
|
const continueSentRef = useRef(false)
|
|
21
23
|
|
|
22
|
-
|
|
24
|
+
// Resolve matching server-side tool_access approval when user grants/denies inline
|
|
25
|
+
const resolveMatchingApproval = (toolId: string, approved: boolean) => {
|
|
26
|
+
const match = Object.values(serverApprovals).find(
|
|
27
|
+
(a) => a.status === 'pending' && a.category === 'tool_access'
|
|
28
|
+
&& (a.data?.toolId === toolId || a.data?.pluginId === toolId)
|
|
29
|
+
)
|
|
30
|
+
if (match) {
|
|
31
|
+
api('POST', '/approvals', { id: match.id, approved }).then(() => loadApprovals()).catch(() => { /* best effort */ })
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pluginRequests: { pluginId: string; reason: string }[] = []
|
|
23
36
|
const seen = new Set<string>()
|
|
24
37
|
|
|
25
38
|
function extractFromText(t: string) {
|
|
26
39
|
try {
|
|
27
|
-
const jsonMatches = t.match(/\{"type"\s*:\s*"tool_request"[^}]*\}/g)
|
|
40
|
+
const jsonMatches = t.match(/\{"type"\s*:\s*"(?:tool_request|plugin_request)"[^}]*\}/g)
|
|
28
41
|
if (jsonMatches) {
|
|
29
42
|
for (const jm of jsonMatches) {
|
|
30
43
|
const parsed = JSON.parse(jm)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
44
|
+
const pluginId = parsed.pluginId || parsed.toolId
|
|
45
|
+
if ((parsed.type === 'tool_request' || parsed.type === 'plugin_request') && pluginId && !seen.has(pluginId)) {
|
|
46
|
+
seen.add(pluginId)
|
|
47
|
+
pluginRequests.push({ pluginId, reason: parsed.reason || '' })
|
|
34
48
|
}
|
|
35
49
|
}
|
|
36
50
|
}
|
|
@@ -41,7 +55,7 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
41
55
|
extractFromText(text)
|
|
42
56
|
for (const output of toolOutputs) extractFromText(output)
|
|
43
57
|
|
|
44
|
-
if (
|
|
58
|
+
if (pluginRequests.length === 0) return null
|
|
45
59
|
|
|
46
60
|
const sid = currentSessionId
|
|
47
61
|
const session = sid ? sessions[sid] : null
|
|
@@ -59,17 +73,20 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
59
73
|
const newGranted = new Set(granted).add(toolId)
|
|
60
74
|
setGranted(newGranted)
|
|
61
75
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
76
|
+
// Resolve matching server-side approval so approvals page stays in sync
|
|
77
|
+
resolveMatchingApproval(toolId, true)
|
|
78
|
+
|
|
79
|
+
// Notify agent that access was granted with a precise message (not a vague "Continue")
|
|
80
|
+
const allGranted = pluginRequests.every(
|
|
81
|
+
(r) => newGranted.has(r.pluginId) || updated.includes(r.pluginId),
|
|
65
82
|
)
|
|
66
83
|
if (allGranted && !continueSentRef.current) {
|
|
67
84
|
continueSentRef.current = true
|
|
68
|
-
|
|
85
|
+
const grantedNames = pluginRequests.map((r) => TOOL_LABELS[r.pluginId] || r.pluginId).join(', ')
|
|
69
86
|
setTimeout(() => {
|
|
70
87
|
const { streaming, sendMessage } = useChatStore.getState()
|
|
71
88
|
if (!streaming) {
|
|
72
|
-
sendMessage(
|
|
89
|
+
sendMessage(`Access granted for: ${grantedNames}. You now have these tools available — proceed with your task.`)
|
|
73
90
|
}
|
|
74
91
|
}, 300)
|
|
75
92
|
}
|
|
@@ -77,24 +94,26 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
77
94
|
|
|
78
95
|
const handleDeny = (toolId: string) => {
|
|
79
96
|
setDenied((prev) => new Set(prev).add(toolId))
|
|
97
|
+
// Resolve matching server-side approval
|
|
98
|
+
resolveMatchingApproval(toolId, false)
|
|
80
99
|
const label = TOOL_LABELS[toolId] || toolId
|
|
81
100
|
setTimeout(() => {
|
|
82
101
|
const { streaming, sendMessage } = useChatStore.getState()
|
|
83
102
|
if (!streaming) {
|
|
84
|
-
sendMessage(`
|
|
103
|
+
sendMessage(`Plugin access denied for ${label} — proceed without it.`)
|
|
85
104
|
}
|
|
86
105
|
}, 200)
|
|
87
106
|
}
|
|
88
107
|
|
|
89
108
|
return (
|
|
90
109
|
<div className="max-w-[85%] md:max-w-[72%] flex flex-col gap-2 mt-2">
|
|
91
|
-
{
|
|
92
|
-
const isGranted = granted.has(
|
|
93
|
-
const isDenied = denied.has(
|
|
94
|
-
const label = TOOL_LABELS[
|
|
110
|
+
{pluginRequests.map(({ pluginId, reason }) => {
|
|
111
|
+
const isGranted = granted.has(pluginId) || (session?.tools || []).includes(pluginId)
|
|
112
|
+
const isDenied = denied.has(pluginId)
|
|
113
|
+
const label = TOOL_LABELS[pluginId] || pluginId
|
|
95
114
|
return (
|
|
96
115
|
<div
|
|
97
|
-
key={
|
|
116
|
+
key={pluginId}
|
|
98
117
|
className="flex items-center gap-3 px-4 py-3 rounded-[12px] border border-amber-500/20 bg-amber-500/[0.06]"
|
|
99
118
|
style={{ animation: 'fade-in 0.2s ease' }}
|
|
100
119
|
>
|
|
@@ -103,7 +122,7 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
103
122
|
</svg>
|
|
104
123
|
<div className="flex-1 min-w-0">
|
|
105
124
|
<p className="text-[12px] text-text-2 font-600">
|
|
106
|
-
Requesting access to <span className="text-amber-400">{label}</span>
|
|
125
|
+
Requesting plugin access to <span className="text-amber-400">{label}</span>
|
|
107
126
|
</p>
|
|
108
127
|
{reason && <p className="text-[11px] text-text-3/60 mt-0.5 truncate">{reason}</p>}
|
|
109
128
|
</div>
|
|
@@ -114,14 +133,14 @@ export function ToolRequestBanner({ text, toolOutputs = [] }: Props) {
|
|
|
114
133
|
) : (
|
|
115
134
|
<div className="flex gap-1.5 shrink-0">
|
|
116
135
|
<button
|
|
117
|
-
onClick={() => handleGrant(
|
|
136
|
+
onClick={() => handleGrant(pluginId)}
|
|
118
137
|
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
138
|
style={{ fontFamily: 'inherit' }}
|
|
120
139
|
>
|
|
121
140
|
Grant
|
|
122
141
|
</button>
|
|
123
142
|
<button
|
|
124
|
-
onClick={() => handleDeny(
|
|
143
|
+
onClick={() => handleDeny(pluginId)}
|
|
125
144
|
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
145
|
style={{ fontFamily: 'inherit' }}
|
|
127
146
|
>
|
|
@@ -63,7 +63,7 @@ export function ChatroomList() {
|
|
|
63
63
|
) : (
|
|
64
64
|
<div className="p-3 space-y-1">
|
|
65
65
|
{sorted.length > 2 && (
|
|
66
|
-
<div className="flex items-center gap-1 px-1 pb-2">
|
|
66
|
+
<div className="flex items-center gap-1 px-1 pb-2" style={{ animation: 'fade-up 0.4s var(--ease-spring)' }}>
|
|
67
67
|
{(['all', 'active', 'recent'] as const).map((f) => (
|
|
68
68
|
<button
|
|
69
69
|
key={f}
|
|
@@ -79,7 +79,7 @@ export function ChatroomList() {
|
|
|
79
79
|
))}
|
|
80
80
|
</div>
|
|
81
81
|
)}
|
|
82
|
-
{filtered.map((chatroom) => {
|
|
82
|
+
{filtered.map((chatroom, idx) => {
|
|
83
83
|
const isActive = chatroom.id === currentChatroomId
|
|
84
84
|
const memberNames = chatroom.agentIds
|
|
85
85
|
.map((id) => agents[id]?.name)
|
|
@@ -91,11 +91,15 @@ export function ChatroomList() {
|
|
|
91
91
|
<button
|
|
92
92
|
key={chatroom.id}
|
|
93
93
|
onClick={() => setCurrentChatroom(chatroom.id)}
|
|
94
|
-
className={`w-full text-left py-3.5 px-4 rounded-[14px] transition-all cursor-pointer group border border-transparent ${
|
|
94
|
+
className={`w-full text-left py-3.5 px-4 rounded-[14px] transition-all cursor-pointer group border border-transparent relative overflow-hidden ${
|
|
95
95
|
isActive
|
|
96
96
|
? 'bg-accent-soft/60'
|
|
97
|
-
: 'hover:bg-white/[0.04]'
|
|
97
|
+
: 'hover:bg-white/[0.04] hover:scale-[1.01]'
|
|
98
98
|
}`}
|
|
99
|
+
style={{
|
|
100
|
+
animation: 'fade-up 0.4s var(--ease-spring) both',
|
|
101
|
+
animationDelay: `${idx * 0.03}s`
|
|
102
|
+
}}
|
|
99
103
|
>
|
|
100
104
|
<div className="flex items-center gap-2 mb-0.5">
|
|
101
105
|
<div className="w-7 h-7 rounded-full bg-accent-soft flex items-center justify-center shrink-0">
|
|
@@ -106,6 +110,9 @@ export function ChatroomList() {
|
|
|
106
110
|
<span className={`text-[13px] font-600 truncate ${isActive ? 'text-accent-bright' : 'text-text'}`}>
|
|
107
111
|
{chatroom.name}
|
|
108
112
|
</span>
|
|
113
|
+
{isActive && (
|
|
114
|
+
<div className="absolute left-0 top-3 bottom-3 w-1 rounded-r-full bg-accent-bright" style={{ animation: 'spring-in 0.3s var(--ease-spring)' }} />
|
|
115
|
+
)}
|
|
109
116
|
<span className="label-mono ml-auto shrink-0">
|
|
110
117
|
{chatroom.agentIds.length} agents
|
|
111
118
|
</span>
|
|
@@ -202,13 +202,15 @@ export function ChatroomSheet() {
|
|
|
202
202
|
}
|
|
203
203
|
if (editing) {
|
|
204
204
|
await updateChatroom(editing.id, payload)
|
|
205
|
-
toast.success('Chatroom
|
|
205
|
+
toast.success('Chatroom updated successfully')
|
|
206
206
|
} else {
|
|
207
207
|
const chatroom = await createChatroom(payload)
|
|
208
208
|
setCurrentChatroom(chatroom.id)
|
|
209
|
-
toast.success('Chatroom created')
|
|
209
|
+
toast.success('Chatroom created successfully')
|
|
210
210
|
}
|
|
211
211
|
setChatroomSheetOpen(false)
|
|
212
|
+
} catch (err: unknown) {
|
|
213
|
+
toast.error(err instanceof Error ? err.message : 'Failed to save chatroom')
|
|
212
214
|
} finally {
|
|
213
215
|
setSaving(false)
|
|
214
216
|
}
|
|
@@ -216,11 +218,14 @@ export function ChatroomSheet() {
|
|
|
216
218
|
|
|
217
219
|
const handleDelete = async () => {
|
|
218
220
|
if (!editing || saving) return
|
|
221
|
+
if (!confirm(`Delete chatroom "${editing.name}"?`)) return
|
|
219
222
|
setSaving(true)
|
|
220
223
|
try {
|
|
221
224
|
await deleteChatroom(editing.id)
|
|
222
225
|
toast.success('Chatroom deleted')
|
|
223
226
|
setChatroomSheetOpen(false)
|
|
227
|
+
} catch (err: unknown) {
|
|
228
|
+
toast.error(err instanceof Error ? err.message : 'Failed to delete chatroom')
|
|
224
229
|
} finally {
|
|
225
230
|
setSaving(false)
|
|
226
231
|
}
|
|
@@ -31,6 +31,10 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
31
31
|
const [reconnecting, setReconnecting] = useState<string | null>(null)
|
|
32
32
|
const [loaded, setLoaded] = useState(false)
|
|
33
33
|
const [error, setError] = useState<string | null>(null)
|
|
34
|
+
const openConnector = useCallback((id: string | null) => {
|
|
35
|
+
setEditingConnectorId(id)
|
|
36
|
+
setConnectorSheetOpen(true)
|
|
37
|
+
}, [setEditingConnectorId, setConnectorSheetOpen])
|
|
34
38
|
|
|
35
39
|
const refresh = useCallback(async () => {
|
|
36
40
|
await Promise.all([loadConnectors(), loadAgents(), loadChatrooms()])
|
|
@@ -95,7 +99,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
95
99
|
<div className="flex-1 flex flex-col items-center justify-center px-6 py-12 text-center">
|
|
96
100
|
<p className="text-[13px] text-text-3">No connectors configured yet.</p>
|
|
97
101
|
<button
|
|
98
|
-
onClick={() =>
|
|
102
|
+
onClick={() => openConnector(null)}
|
|
99
103
|
className="mt-3 text-[13px] text-accent-bright hover:underline cursor-pointer bg-transparent border-none"
|
|
100
104
|
>
|
|
101
105
|
+ Add Connector
|
|
@@ -113,7 +117,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
113
117
|
{error}
|
|
114
118
|
</div>
|
|
115
119
|
)}
|
|
116
|
-
{list.map((c) => {
|
|
120
|
+
{list.map((c, idx) => {
|
|
117
121
|
const agent = c.agentId ? agents[c.agentId] : null
|
|
118
122
|
const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
|
|
119
123
|
const isRunning = c.status === 'running'
|
|
@@ -121,8 +125,12 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
121
125
|
return (
|
|
122
126
|
<button
|
|
123
127
|
key={c.id}
|
|
124
|
-
onClick={() =>
|
|
128
|
+
onClick={() => openConnector(c.id)}
|
|
125
129
|
className="w-full flex items-center gap-3 px-5 py-2.5 hover:bg-white/[0.02] transition-colors cursor-pointer bg-transparent border-none text-left"
|
|
130
|
+
style={{
|
|
131
|
+
animation: 'fade-up 0.4s var(--ease-spring) both',
|
|
132
|
+
animationDelay: `${idx * 0.03}s`
|
|
133
|
+
}}
|
|
126
134
|
>
|
|
127
135
|
<ConnectorPlatformIcon platform={c.platform} size={16} />
|
|
128
136
|
<div className="flex-1 min-w-0">
|
|
@@ -133,7 +141,8 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
133
141
|
</div>
|
|
134
142
|
<span className={`shrink-0 w-2 h-2 rounded-full ${
|
|
135
143
|
isRunning ? 'bg-green-400' : c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
|
|
136
|
-
}`}
|
|
144
|
+
}`}
|
|
145
|
+
style={isRunning ? { animation: 'pulse-subtle 2s infinite' } : undefined} />
|
|
137
146
|
</button>
|
|
138
147
|
)
|
|
139
148
|
})}
|
|
@@ -150,7 +159,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
150
159
|
</div>
|
|
151
160
|
)}
|
|
152
161
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
|
153
|
-
{list.map((c) => {
|
|
162
|
+
{list.map((c, idx) => {
|
|
154
163
|
const platformLabel = getConnectorPlatformLabel(c.platform)
|
|
155
164
|
const agent = c.agentId ? agents[c.agentId] : null
|
|
156
165
|
const chatroom = c.chatroomId ? chatrooms[c.chatroomId] : null
|
|
@@ -164,11 +173,23 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
164
173
|
const lastMsg = c.presence?.lastMessageAt
|
|
165
174
|
|
|
166
175
|
return (
|
|
167
|
-
<
|
|
176
|
+
<div
|
|
168
177
|
key={c.id}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
role="button"
|
|
179
|
+
tabIndex={0}
|
|
180
|
+
onClick={() => openConnector(c.id)}
|
|
181
|
+
onKeyDown={(e) => {
|
|
182
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
183
|
+
e.preventDefault()
|
|
184
|
+
openConnector(c.id)
|
|
185
|
+
}
|
|
186
|
+
}}
|
|
187
|
+
className="group relative flex flex-col rounded-[14px] border border-white/[0.06] bg-surface p-4 cursor-pointer transition-all hover:border-white/[0.12] hover:bg-white/[0.02] hover:scale-[1.01] text-left w-full"
|
|
188
|
+
style={{
|
|
189
|
+
fontFamily: 'inherit',
|
|
190
|
+
animation: 'spring-in 0.5s var(--ease-spring) both',
|
|
191
|
+
animationDelay: `${idx * 0.05}s`
|
|
192
|
+
}}
|
|
172
193
|
>
|
|
173
194
|
{/* Header: platform badge + status */}
|
|
174
195
|
<div className="flex items-center gap-3 mb-3">
|
|
@@ -178,7 +199,8 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
178
199
|
<span className="text-[14px] font-600 text-text truncate">{c.name}</span>
|
|
179
200
|
<span className={`shrink-0 w-2 h-2 rounded-full ${
|
|
180
201
|
isRunning ? 'bg-green-400' : c.status === 'error' ? 'bg-red-400' : 'bg-white/20'
|
|
181
|
-
}`}
|
|
202
|
+
}`}
|
|
203
|
+
style={isRunning ? { animation: 'pulse-subtle 2s infinite' } : c.status === 'error' ? { animation: 'ai-shake 0.5s' } : undefined} />
|
|
182
204
|
</div>
|
|
183
205
|
<span className="text-[11px] text-text-3 block">
|
|
184
206
|
{isRunning ? 'Connected' : c.status === 'error' ? 'Error' : 'Stopped'}
|
|
@@ -264,7 +286,7 @@ export function ConnectorList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
264
286
|
)}
|
|
265
287
|
</div>
|
|
266
288
|
</div>
|
|
267
|
-
</
|
|
289
|
+
</div>
|
|
268
290
|
)
|
|
269
291
|
})}
|
|
270
292
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect, useCallback } from 'react'
|
|
3
|
+
import { useState, useEffect, useCallback, useMemo } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
6
6
|
import { api } from '@/lib/api-client'
|
|
@@ -242,6 +242,29 @@ const COMMON_CONFIG_FIELDS: { key: string; label: string; placeholder: string; h
|
|
|
242
242
|
export function ConnectorSheet() {
|
|
243
243
|
const open = useAppStore((s) => s.connectorSheetOpen)
|
|
244
244
|
const setOpen = useAppStore((s) => s.setConnectorSheetOpen)
|
|
245
|
+
// ... (existing state)
|
|
246
|
+
const [dynamicPlatforms, setDynamicPlatforms] = useState<Array<{ id: string; name: string; description?: string }>>([])
|
|
247
|
+
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (open) {
|
|
250
|
+
api<Array<{ id: string; name: string; description?: string }>>('GET', '/plugins/ui?type=connectors').then(list => {
|
|
251
|
+
setDynamicPlatforms(list || [])
|
|
252
|
+
}).catch(() => {})
|
|
253
|
+
}
|
|
254
|
+
}, [open])
|
|
255
|
+
|
|
256
|
+
const ALL_PLATFORMS = useMemo(() => {
|
|
257
|
+
const plugins = dynamicPlatforms.map(p => ({
|
|
258
|
+
id: p.id,
|
|
259
|
+
label: p.name,
|
|
260
|
+
color: '#10B981',
|
|
261
|
+
setupSteps: [p.description || 'Follow the plugin instructions for setup.'],
|
|
262
|
+
tokenLabel: 'Plugin Token',
|
|
263
|
+
tokenHelp: 'Secret key required by this plugin connector.',
|
|
264
|
+
configFields: [],
|
|
265
|
+
}))
|
|
266
|
+
return [...PLATFORMS, ...plugins]
|
|
267
|
+
}, [dynamicPlatforms])
|
|
245
268
|
const editingId = useAppStore((s) => s.editingConnectorId)
|
|
246
269
|
const setEditingId = useAppStore((s) => s.setEditingConnectorId)
|
|
247
270
|
const connectors = useAppStore((s) => s.connectors)
|
|
@@ -393,7 +416,7 @@ export function ConnectorSheet() {
|
|
|
393
416
|
setEditingId(null)
|
|
394
417
|
}
|
|
395
418
|
|
|
396
|
-
const platformConfig =
|
|
419
|
+
const platformConfig = ALL_PLATFORMS.find((p) => p.id === platform) || ALL_PLATFORMS[0]
|
|
397
420
|
const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
|
|
398
421
|
const credList = Object.values(credentials)
|
|
399
422
|
|
|
@@ -413,17 +436,17 @@ export function ConnectorSheet() {
|
|
|
413
436
|
<div className="mb-8">
|
|
414
437
|
<SectionLabel>Platform</SectionLabel>
|
|
415
438
|
<div className="grid grid-cols-2 gap-3">
|
|
416
|
-
{
|
|
439
|
+
{ALL_PLATFORMS.map((p) => (
|
|
417
440
|
<button
|
|
418
441
|
key={p.id}
|
|
419
|
-
onClick={() => { setPlatform(p.id); setShowSetup(false) }}
|
|
442
|
+
onClick={() => { setPlatform(p.id as ConnectorPlatform); setShowSetup(false) }}
|
|
420
443
|
className={`flex items-center gap-3 p-4 rounded-[14px] cursor-pointer transition-all duration-200 border text-left
|
|
421
444
|
${platform === p.id
|
|
422
445
|
? 'bg-white/[0.04] border-white/[0.15] shadow-[0_0_20px_rgba(255,255,255,0.02)]'
|
|
423
446
|
: 'bg-transparent border-white/[0.04] hover:border-white/[0.08] hover:bg-white/[0.01]'}`}
|
|
424
447
|
style={{ fontFamily: 'inherit' }}
|
|
425
448
|
>
|
|
426
|
-
<ConnectorPlatformBadge platform={p.id} size={40} iconSize={18} />
|
|
449
|
+
<ConnectorPlatformBadge platform={p.id as ConnectorPlatform} size={40} iconSize={18} />
|
|
427
450
|
<div>
|
|
428
451
|
<div className={`text-[14px] font-600 ${platform === p.id ? 'text-text' : 'text-text-2'}`}>{p.label}</div>
|
|
429
452
|
<div className="text-[11px] text-text-3 mt-0.5">
|
|
@@ -439,7 +462,7 @@ export function ConnectorSheet() {
|
|
|
439
462
|
{/* Editing: show platform badge */}
|
|
440
463
|
{editing && (
|
|
441
464
|
<div className="mb-6 flex items-center gap-3">
|
|
442
|
-
<ConnectorPlatformBadge platform={platformConfig.id} size={40} iconSize={18} />
|
|
465
|
+
<ConnectorPlatformBadge platform={platformConfig.id as ConnectorPlatform} size={40} iconSize={18} />
|
|
443
466
|
<div>
|
|
444
467
|
<div className="text-[14px] font-600 text-text">{platformConfig.label}</div>
|
|
445
468
|
<div className="flex items-center gap-2 mt-0.5">
|
|
@@ -192,7 +192,7 @@ export function HomeView() {
|
|
|
192
192
|
<div className="flex-1 overflow-y-auto">
|
|
193
193
|
<div className="max-w-[800px] mx-auto px-6 py-10">
|
|
194
194
|
{/* Header */}
|
|
195
|
-
<div className="mb-10">
|
|
195
|
+
<div className="mb-10" style={{ animation: 'spring-in 0.6s var(--ease-spring)' }}>
|
|
196
196
|
<h1 className="font-display text-[28px] font-700 text-text tracking-[-0.03em]">
|
|
197
197
|
SwarmClaw
|
|
198
198
|
</h1>
|
|
@@ -203,15 +203,15 @@ export function HomeView() {
|
|
|
203
203
|
|
|
204
204
|
{/* Quick Stats */}
|
|
205
205
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
|
206
|
-
<StatCard label="Agents" value={String(agentCount)} hint="Total active agents configured in your dashboard" />
|
|
207
|
-
<StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} hint="Tasks currently running or queued for execution" />
|
|
208
|
-
<StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} hint="Estimated API cost for today across all providers" />
|
|
209
|
-
<StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} hint="Active bridges to chat platforms (Discord, Slack, etc.)" />
|
|
206
|
+
<StatCard label="Agents" value={String(agentCount)} hint="Total active agents configured in your dashboard" index={0} />
|
|
207
|
+
<StatCard label="Active Tasks" value={String(activeTaskCount)} accent={activeTaskCount > 0} hint="Tasks currently running or queued for execution" index={1} />
|
|
208
|
+
<StatCard label="Today's Cost" value={`$${todayCost.toFixed(2)}`} hint="Estimated API cost for today across all providers" index={2} />
|
|
209
|
+
<StatCard label="Connectors" value={`${activeConnectorCount}/${allConnectors.length}`} accent={activeConnectorCount > 0} hint="Active bridges to chat platforms (Discord, Slack, etc.)" index={3} />
|
|
210
210
|
</div>
|
|
211
211
|
|
|
212
212
|
{/* Cost trend sparkline */}
|
|
213
213
|
{costTrend.length > 1 && (
|
|
214
|
-
<div className="mb-10 px-1">
|
|
214
|
+
<div className="mb-10 px-1" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
|
|
215
215
|
<p className="text-[10px] text-text-3/50 uppercase tracking-wider mb-1 flex items-center gap-1.5">
|
|
216
216
|
7-day cost trend <HintTip text="Daily API spend over the past week — hover for details" />
|
|
217
217
|
</p>
|
|
@@ -247,7 +247,7 @@ export function HomeView() {
|
|
|
247
247
|
|
|
248
248
|
{/* Notifications banner */}
|
|
249
249
|
{unreadNotifications.length > 0 && (
|
|
250
|
-
<section className="mb-8">
|
|
250
|
+
<section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.35s both' }}>
|
|
251
251
|
<div className="rounded-[14px] border border-amber-400/20 bg-amber-400/[0.04] overflow-hidden">
|
|
252
252
|
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-amber-400/10">
|
|
253
253
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400">
|
|
@@ -283,7 +283,7 @@ export function HomeView() {
|
|
|
283
283
|
)}
|
|
284
284
|
|
|
285
285
|
{/* Connector Status */}
|
|
286
|
-
<section className="mb-8">
|
|
286
|
+
<section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}>
|
|
287
287
|
<SectionHeader label="Connectors" onViewAll={allConnectors.length > 0 ? () => setActiveView('connectors') : undefined} />
|
|
288
288
|
{allConnectors.length > 0 ? (
|
|
289
289
|
<div className="flex gap-2 flex-wrap">
|
|
@@ -307,7 +307,7 @@ export function HomeView() {
|
|
|
307
307
|
</section>
|
|
308
308
|
|
|
309
309
|
{/* Two-column layout: Running Tasks + Upcoming Schedules */}
|
|
310
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8">
|
|
310
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.45s both' }}>
|
|
311
311
|
{/* Running Tasks */}
|
|
312
312
|
<section>
|
|
313
313
|
<SectionHeader label="Running Tasks" onViewAll={runningTasks.length > 0 ? () => setActiveView('tasks') : undefined} />
|
|
@@ -373,7 +373,7 @@ export function HomeView() {
|
|
|
373
373
|
</div>
|
|
374
374
|
|
|
375
375
|
{/* Pinned Agents */}
|
|
376
|
-
<section className="mb-8">
|
|
376
|
+
<section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.5s both' }}>
|
|
377
377
|
<SectionHeader label="Pinned Agents" />
|
|
378
378
|
{pinnedAgents.length > 0 ? (
|
|
379
379
|
<div className="flex gap-3 overflow-x-auto pb-2">
|
|
@@ -438,7 +438,7 @@ export function HomeView() {
|
|
|
438
438
|
</section>
|
|
439
439
|
|
|
440
440
|
{/* Recent Chats */}
|
|
441
|
-
<section className="mb-8">
|
|
441
|
+
<section className="mb-8" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.55s both' }}>
|
|
442
442
|
<SectionHeader label="Recent Chats" />
|
|
443
443
|
{recentChats.length > 0 ? (
|
|
444
444
|
<div className="flex flex-col gap-1">
|
|
@@ -486,7 +486,7 @@ export function HomeView() {
|
|
|
486
486
|
|
|
487
487
|
{/* Activity Feed */}
|
|
488
488
|
{recentActivity.length > 0 && (
|
|
489
|
-
<section className="mb-10">
|
|
489
|
+
<section className="mb-10" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.6s both' }}>
|
|
490
490
|
<SectionHeader label="Recent Activity" />
|
|
491
491
|
<div className="flex flex-col gap-0.5">
|
|
492
492
|
{recentActivity.map((entry) => (
|
|
@@ -526,9 +526,15 @@ function SectionHeader({ label, onViewAll }: { label: string; onViewAll?: () =>
|
|
|
526
526
|
)
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
-
function StatCard({ label, value, accent, hint }: { label: string; value: string; accent?: boolean; hint?: string }) {
|
|
529
|
+
function StatCard({ label, value, accent, hint, index = 0 }: { label: string; value: string; accent?: boolean; hint?: string; index?: number }) {
|
|
530
530
|
return (
|
|
531
|
-
<div
|
|
531
|
+
<div
|
|
532
|
+
className="px-4 py-3 rounded-[12px] bg-white/[0.03] border border-white/[0.06] hover:bg-white/[0.05] transition-all hover:scale-[1.02] active:scale-[0.98] cursor-default"
|
|
533
|
+
style={{
|
|
534
|
+
animation: 'spring-in 0.6s var(--ease-spring) both',
|
|
535
|
+
animationDelay: `${0.1 + index * 0.05}s`
|
|
536
|
+
}}
|
|
537
|
+
>
|
|
532
538
|
<p className="text-[11px] font-600 text-text-3/60 uppercase tracking-wider mb-1 flex items-center gap-1.5">
|
|
533
539
|
{label}
|
|
534
540
|
{hint && <HintTip text={hint} />}
|
|
@@ -15,13 +15,14 @@ interface Props {
|
|
|
15
15
|
streaming: boolean
|
|
16
16
|
onSend: (text: string) => void
|
|
17
17
|
onStop: () => void
|
|
18
|
+
pluginChatActions?: Array<{ id: string; label: string; action: string; value: string; tooltip?: string }>
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// FilePreview is now imported from @/components/shared/file-preview
|
|
21
22
|
|
|
22
23
|
const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
|
|
23
24
|
|
|
24
|
-
export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
25
|
+
export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }: Props) {
|
|
25
26
|
const [value, setValue] = useState('')
|
|
26
27
|
const { ref: textareaRef, resize } = useAutoResize()
|
|
27
28
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
@@ -219,6 +220,26 @@ export function ChatInput({ streaming, onSend, onStop }: Props) {
|
|
|
219
220
|
<span className="hidden sm:inline">Image</span>
|
|
220
221
|
</button>
|
|
221
222
|
|
|
223
|
+
{/* Plugin Chat Actions */}
|
|
224
|
+
{pluginChatActions.map((action) => (
|
|
225
|
+
<Tooltip key={action.id}>
|
|
226
|
+
<TooltipTrigger asChild>
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => {
|
|
229
|
+
if (action.action === 'message') onSend(action.value)
|
|
230
|
+
else if (action.action === 'link') window.open(action.value, '_blank')
|
|
231
|
+
}}
|
|
232
|
+
className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-emerald-500/[0.05]
|
|
233
|
+
text-emerald-400 text-[13px] cursor-pointer hover:text-emerald-300 hover:bg-emerald-500/[0.1] transition-all duration-200"
|
|
234
|
+
style={{ fontFamily: 'inherit' }}
|
|
235
|
+
>
|
|
236
|
+
{action.label}
|
|
237
|
+
</button>
|
|
238
|
+
</TooltipTrigger>
|
|
239
|
+
{action.tooltip && <TooltipContent>{action.tooltip}</TooltipContent>}
|
|
240
|
+
</Tooltip>
|
|
241
|
+
))}
|
|
242
|
+
|
|
222
243
|
{micSupported && (
|
|
223
244
|
<button
|
|
224
245
|
onClick={toggleRecording}
|