@swarmclawai/swarmclaw 0.3.0 → 0.4.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 +20 -11
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +2 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +3 -0
- package/src/app/api/agents/[id]/thread/route.ts +2 -1
- package/src/app/api/agents/route.ts +5 -1
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/connectors/[id]/route.ts +4 -0
- package/src/app/api/connectors/route.ts +6 -1
- package/src/app/api/credentials/route.ts +3 -1
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/mcp-servers/route.ts +3 -1
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/providers/[id]/route.ts +3 -0
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +5 -1
- package/src/app/api/schedules/[id]/route.ts +3 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/route.ts +3 -1
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/route.ts +9 -2
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -1
- package/src/app/api/tasks/[id]/approve/route.ts +73 -0
- package/src/app/api/tasks/[id]/route.ts +3 -0
- package/src/app/api/tasks/route.ts +3 -0
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.ts +3 -1
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +8 -2
- package/src/cli/index.js +1 -9
- package/src/cli/index.ts +51 -1
- package/src/cli/spec.js +0 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-sheet.tsx +63 -80
- package/src/components/chat/chat-area.tsx +44 -30
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +41 -3
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/connectors/connector-list.tsx +3 -8
- package/src/components/connectors/connector-sheet.tsx +24 -29
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +92 -71
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +6 -3
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +8 -9
- package/src/components/shared/connector-platform-icon.tsx +22 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +7 -39
- package/src/components/shared/settings/section-orchestrator.tsx +8 -9
- package/src/components/skills/skill-list.tsx +260 -34
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +3 -5
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/providers/anthropic.ts +1 -1
- package/src/lib/providers/index.ts +2 -0
- package/src/lib/providers/ollama.ts +1 -1
- package/src/lib/providers/openai.ts +33 -12
- package/src/lib/server/chat-execution.ts +19 -4
- package/src/lib/server/connectors/manager.ts +9 -3
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +61 -2
- package/src/lib/server/orchestrator-lg.ts +394 -13
- package/src/lib/server/orchestrator.ts +25 -5
- package/src/lib/server/queue.ts +17 -3
- package/src/lib/server/session-run-manager.ts +6 -1
- package/src/lib/server/session-tools/delegate.ts +2 -2
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/sandbox.ts +164 -0
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +24 -7
- package/src/lib/server/stream-agent-chat.ts +77 -22
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +42 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/ws-client.ts +124 -0
- package/src/stores/use-chat-store.ts +33 -13
- package/src/types/index.ts +8 -1
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
|
@@ -5,38 +5,10 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { createAgent, updateAgent, deleteAgent } from '@/lib/agents'
|
|
6
6
|
import { api } from '@/lib/api-client'
|
|
7
7
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
8
|
-
import { AiGenBlock } from '@/components/shared/ai-gen-block'
|
|
9
8
|
import { toast } from 'sonner'
|
|
9
|
+
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
10
10
|
import type { ProviderType, ClaudeSkill } from '@/types'
|
|
11
|
-
|
|
12
|
-
const AVAILABLE_TOOLS: { id: string; label: string; description: string }[] = [
|
|
13
|
-
{ id: 'shell', label: 'Shell', description: 'Execute commands in the working directory' },
|
|
14
|
-
{ id: 'files', label: 'Files', description: 'Read, write, list, move, copy, and send files' },
|
|
15
|
-
{ id: 'copy_file', label: 'Copy File', description: 'Copy files within the working directory' },
|
|
16
|
-
{ id: 'move_file', label: 'Move File', description: 'Move/rename files within the working directory' },
|
|
17
|
-
{ id: 'delete_file', label: 'Delete File', description: 'Delete files/directories (disabled by default)' },
|
|
18
|
-
{ id: 'edit_file', label: 'Edit File', description: 'Search-and-replace editing within files' },
|
|
19
|
-
{ id: 'process', label: 'Process', description: 'Monitor and control long-running shell commands' },
|
|
20
|
-
{ id: 'web_search', label: 'Web Search', description: 'Search the web via DuckDuckGo' },
|
|
21
|
-
{ id: 'web_fetch', label: 'Web Fetch', description: 'Fetch and extract text from URLs' },
|
|
22
|
-
{ id: 'claude_code', label: 'Claude Code', description: 'Delegate complex tasks to Claude Code CLI' },
|
|
23
|
-
{ id: 'codex_cli', label: 'Codex CLI', description: 'Delegate complex tasks to OpenAI Codex CLI' },
|
|
24
|
-
{ id: 'opencode_cli', label: 'OpenCode CLI', description: 'Delegate complex tasks to OpenCode CLI' },
|
|
25
|
-
{ id: 'browser', label: 'Browser', description: 'Playwright — browse, scrape, interact with web pages' },
|
|
26
|
-
{ id: 'memory', label: 'Memory', description: 'Store and retrieve long-term memories across sessions' },
|
|
27
|
-
]
|
|
28
|
-
|
|
29
|
-
const PLATFORM_TOOLS: { id: string; label: string; description: string }[] = [
|
|
30
|
-
{ id: 'manage_agents', label: 'Agents', description: 'Create, edit, and delete agents' },
|
|
31
|
-
{ id: 'manage_tasks', label: 'Tasks', description: 'Create, edit, and delete tasks' },
|
|
32
|
-
{ id: 'manage_schedules', label: 'Schedules', description: 'Create, edit, and delete schedules' },
|
|
33
|
-
{ id: 'manage_skills', label: 'Skills', description: 'Create, edit, and delete skills' },
|
|
34
|
-
{ id: 'manage_documents', label: 'Documents', description: 'Upload, search, and delete indexed documents' },
|
|
35
|
-
{ id: 'manage_webhooks', label: 'Webhooks', description: 'Register webhooks that trigger agent sessions' },
|
|
36
|
-
{ id: 'manage_connectors', label: 'Connectors', description: 'Create, edit, and delete connectors' },
|
|
37
|
-
{ id: 'manage_sessions', label: 'Sessions', description: 'List sessions, send messages, and spawn session work' },
|
|
38
|
-
{ id: 'manage_secrets', label: 'Secrets', description: 'Store and retrieve encrypted service secrets' },
|
|
39
|
-
]
|
|
11
|
+
import { AVAILABLE_TOOLS, PLATFORM_TOOLS } from '@/lib/tool-definitions'
|
|
40
12
|
|
|
41
13
|
const NATIVE_CAPABILITY_PROVIDER_IDS = new Set<ProviderType>(['claude-cli', 'codex-cli', 'opencode-cli', 'openclaw'])
|
|
42
14
|
|
|
@@ -54,9 +26,6 @@ export function AgentSheet() {
|
|
|
54
26
|
const dynamicSkills = useAppStore((s) => s.skills)
|
|
55
27
|
const mcpServers = useAppStore((s) => s.mcpServers)
|
|
56
28
|
const loadSkills = useAppStore((s) => s.loadSkills)
|
|
57
|
-
const appSettings = useAppStore((s) => s.appSettings)
|
|
58
|
-
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
59
|
-
|
|
60
29
|
const [claudeSkills, setClaudeSkills] = useState<ClaudeSkill[]>([])
|
|
61
30
|
const [claudeSkillsLoading, setClaudeSkillsLoading] = useState(false)
|
|
62
31
|
const loadClaudeSkills = async () => {
|
|
@@ -91,6 +60,9 @@ export function AgentSheet() {
|
|
|
91
60
|
const [capInput, setCapInput] = useState('')
|
|
92
61
|
const [ollamaMode, setOllamaMode] = useState<'local' | 'cloud'>('local')
|
|
93
62
|
const [openclawEnabled, setOpenclawEnabled] = useState(false)
|
|
63
|
+
const [heartbeatEnabled, setHeartbeatEnabled] = useState(false)
|
|
64
|
+
const [heartbeatInterval, setHeartbeatInterval] = useState('')
|
|
65
|
+
const [heartbeatModel, setHeartbeatModel] = useState('')
|
|
94
66
|
const [addingKey, setAddingKey] = useState(false)
|
|
95
67
|
const [newKeyName, setNewKeyName] = useState('')
|
|
96
68
|
const [newKeyValue, setNewKeyValue] = useState('')
|
|
@@ -117,12 +89,6 @@ export function AgentSheet() {
|
|
|
117
89
|
e.target.value = ''
|
|
118
90
|
}
|
|
119
91
|
|
|
120
|
-
// AI generation state
|
|
121
|
-
const [aiPrompt, setAiPrompt] = useState('')
|
|
122
|
-
const [generating, setGenerating] = useState(false)
|
|
123
|
-
const [generated, setGenerated] = useState(false)
|
|
124
|
-
const [genError, setGenError] = useState('')
|
|
125
|
-
|
|
126
92
|
const currentProvider = providers.find((p) => p.id === provider)
|
|
127
93
|
const providerCredentials = Object.values(credentials).filter((c) => c.provider === provider)
|
|
128
94
|
const openclawCredentials = Object.values(credentials).filter((c) => c.provider === 'openclaw')
|
|
@@ -140,11 +106,6 @@ export function AgentSheet() {
|
|
|
140
106
|
loadCredentials()
|
|
141
107
|
loadSkills()
|
|
142
108
|
loadClaudeSkills()
|
|
143
|
-
loadSettings()
|
|
144
|
-
setAiPrompt('')
|
|
145
|
-
setGenerating(false)
|
|
146
|
-
setGenerated(false)
|
|
147
|
-
setGenError('')
|
|
148
109
|
setTestStatus('idle')
|
|
149
110
|
setTestMessage('')
|
|
150
111
|
if (editing) {
|
|
@@ -169,6 +130,9 @@ export function AgentSheet() {
|
|
|
169
130
|
setCapInput('')
|
|
170
131
|
setOllamaMode(editing.credentialId && editing.provider === 'ollama' ? 'cloud' : 'local')
|
|
171
132
|
setOpenclawEnabled(editing.provider === 'openclaw')
|
|
133
|
+
setHeartbeatEnabled(editing.heartbeatEnabled || false)
|
|
134
|
+
setHeartbeatInterval(editing.heartbeatInterval != null ? String(editing.heartbeatInterval) : '')
|
|
135
|
+
setHeartbeatModel(editing.heartbeatModel || '')
|
|
172
136
|
} else {
|
|
173
137
|
setName('')
|
|
174
138
|
setDescription('')
|
|
@@ -190,6 +154,9 @@ export function AgentSheet() {
|
|
|
190
154
|
setCapInput('')
|
|
191
155
|
setOllamaMode('local')
|
|
192
156
|
setOpenclawEnabled(false)
|
|
157
|
+
setHeartbeatEnabled(false)
|
|
158
|
+
setHeartbeatInterval('')
|
|
159
|
+
setHeartbeatModel('')
|
|
193
160
|
}
|
|
194
161
|
}
|
|
195
162
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -246,29 +213,6 @@ export function AgentSheet() {
|
|
|
246
213
|
return () => { cancelled = true }
|
|
247
214
|
}, [openclawEnabled])
|
|
248
215
|
|
|
249
|
-
const handleGenerate = async () => {
|
|
250
|
-
if (!aiPrompt.trim()) return
|
|
251
|
-
setGenerating(true)
|
|
252
|
-
setGenError('')
|
|
253
|
-
try {
|
|
254
|
-
const result = await api<{ name?: string; description?: string; systemPrompt?: string; isOrchestrator?: boolean; error?: string }>('POST', '/agents/generate', { prompt: aiPrompt })
|
|
255
|
-
if (result.error) {
|
|
256
|
-
setGenError(result.error)
|
|
257
|
-
} else if (result.name || result.systemPrompt) {
|
|
258
|
-
if (result.name) setName(result.name)
|
|
259
|
-
if (result.description) setDescription(result.description)
|
|
260
|
-
if (result.systemPrompt) setSystemPrompt(result.systemPrompt)
|
|
261
|
-
if (result.isOrchestrator !== undefined) setIsOrchestrator(result.isOrchestrator)
|
|
262
|
-
setGenerated(true)
|
|
263
|
-
} else {
|
|
264
|
-
setGenError('AI returned empty response — try again')
|
|
265
|
-
}
|
|
266
|
-
} catch (err: unknown) {
|
|
267
|
-
setGenError(err instanceof Error ? err.message : 'Generation failed')
|
|
268
|
-
}
|
|
269
|
-
setGenerating(false)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
216
|
const onClose = () => {
|
|
273
217
|
setOpen(false)
|
|
274
218
|
setEditingId(null)
|
|
@@ -300,6 +244,9 @@ export function AgentSheet() {
|
|
|
300
244
|
fallbackCredentialIds,
|
|
301
245
|
platformAssignScope,
|
|
302
246
|
capabilities,
|
|
247
|
+
heartbeatEnabled,
|
|
248
|
+
heartbeatInterval: heartbeatInterval.trim() || null,
|
|
249
|
+
heartbeatModel: heartbeatModel.trim() || null,
|
|
303
250
|
}
|
|
304
251
|
if (editing) {
|
|
305
252
|
await updateAgent(editing.id, data)
|
|
@@ -450,14 +397,6 @@ export function AgentSheet() {
|
|
|
450
397
|
</div>
|
|
451
398
|
</div>
|
|
452
399
|
|
|
453
|
-
{/* AI Generation */}
|
|
454
|
-
{!editing && !openclawEnabled && <AiGenBlock
|
|
455
|
-
aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
|
|
456
|
-
generating={generating} generated={generated} genError={genError}
|
|
457
|
-
onGenerate={handleGenerate} appSettings={appSettings}
|
|
458
|
-
placeholder='Describe the agent you want, e.g. "An SEO keyword researcher that finds low-competition long-tail keywords"'
|
|
459
|
-
/>}
|
|
460
|
-
|
|
461
400
|
<div className="mb-8">
|
|
462
401
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
|
|
463
402
|
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. SEO Researcher" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
@@ -512,6 +451,47 @@ export function AgentSheet() {
|
|
|
512
451
|
<p className="text-[11px] text-text-3/70 mt-1.5">Press Enter or comma to add. Other agents see these when deciding delegation.</p>
|
|
513
452
|
</div>}
|
|
514
453
|
|
|
454
|
+
{/* Heartbeat Configuration */}
|
|
455
|
+
<div className="mb-8">
|
|
456
|
+
<div className="flex items-center justify-between mb-3">
|
|
457
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em]">Heartbeat</label>
|
|
458
|
+
<button
|
|
459
|
+
type="button"
|
|
460
|
+
onClick={() => setHeartbeatEnabled(!heartbeatEnabled)}
|
|
461
|
+
className={`relative w-10 h-[22px] rounded-full transition-colors duration-200 cursor-pointer ${heartbeatEnabled ? 'bg-accent' : 'bg-white/[0.12]'}`}
|
|
462
|
+
>
|
|
463
|
+
<span className={`absolute top-[3px] left-[3px] w-4 h-4 rounded-full bg-white transition-transform duration-200 ${heartbeatEnabled ? 'translate-x-[18px]' : ''}`} />
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
466
|
+
{heartbeatEnabled && (
|
|
467
|
+
<div className="space-y-4 mt-3">
|
|
468
|
+
<div>
|
|
469
|
+
<label className="block text-[12px] text-text-3/70 mb-1.5">Interval</label>
|
|
470
|
+
<input
|
|
471
|
+
type="text"
|
|
472
|
+
value={heartbeatInterval}
|
|
473
|
+
onChange={(e) => setHeartbeatInterval(e.target.value)}
|
|
474
|
+
placeholder="30s, 5m, 1h (default: 30m)"
|
|
475
|
+
className={inputClass}
|
|
476
|
+
style={{ fontFamily: 'inherit' }}
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
<div>
|
|
480
|
+
<label className="block text-[12px] text-text-3/70 mb-1.5">Model override <span className="text-text-3/50">(optional, cheaper model)</span></label>
|
|
481
|
+
<input
|
|
482
|
+
type="text"
|
|
483
|
+
value={heartbeatModel}
|
|
484
|
+
onChange={(e) => setHeartbeatModel(e.target.value)}
|
|
485
|
+
placeholder="e.g. gpt-4o-mini"
|
|
486
|
+
className={inputClass}
|
|
487
|
+
style={{ fontFamily: 'inherit' }}
|
|
488
|
+
/>
|
|
489
|
+
</div>
|
|
490
|
+
</div>
|
|
491
|
+
)}
|
|
492
|
+
<p className="text-[11px] text-text-3/70 mt-1.5">Periodic check-in runs on idle sessions using this agent. Processes pending events and monitors status.</p>
|
|
493
|
+
</div>
|
|
494
|
+
|
|
515
495
|
{provider !== 'openclaw' && (
|
|
516
496
|
<div className="mb-8">
|
|
517
497
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
@@ -774,11 +754,14 @@ export function AgentSheet() {
|
|
|
774
754
|
{!openclawEnabled && currentProvider && currentProvider.models.length > 0 && (
|
|
775
755
|
<div className="mb-8">
|
|
776
756
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Model</label>
|
|
777
|
-
<
|
|
778
|
-
{currentProvider.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
757
|
+
<ModelCombobox
|
|
758
|
+
providerId={currentProvider.id}
|
|
759
|
+
value={model}
|
|
760
|
+
onChange={setModel}
|
|
761
|
+
models={currentProvider.models}
|
|
762
|
+
defaultModels={currentProvider.defaultModels}
|
|
763
|
+
className={`${inputClass} cursor-pointer`}
|
|
764
|
+
/>
|
|
782
765
|
</div>
|
|
783
766
|
)}
|
|
784
767
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useCallback, useState, useRef } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { useWs } from '@/hooks/use-ws'
|
|
5
6
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
6
7
|
import { fetchMessages, clearMessages, deleteSession, devServer, checkBrowser, stopBrowser } from '@/lib/sessions'
|
|
7
8
|
import { uploadImage } from '@/lib/upload'
|
|
@@ -90,33 +91,43 @@ export function ChatArea() {
|
|
|
90
91
|
const isOrchestrated = session?.sessionType === 'orchestrated'
|
|
91
92
|
const isServerActive = session?.active === true
|
|
92
93
|
const isOngoingMonitored = appSettings.loopMode === 'ongoing' && !!session?.tools?.length
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
94
|
+
const shouldPollMessages = !!sessionId && (isOrchestrated || isServerActive || isOngoingMonitored)
|
|
95
|
+
const messagesLenRef = useRef(messages.length)
|
|
96
|
+
messagesLenRef.current = messages.length
|
|
97
|
+
const isServerActiveRef = useRef(isServerActive)
|
|
98
|
+
isServerActiveRef.current = isServerActive
|
|
99
|
+
const ttsEnabledRef = useRef(ttsEnabled)
|
|
100
|
+
ttsEnabledRef.current = ttsEnabled
|
|
101
|
+
|
|
102
|
+
const refreshMessages = useCallback(async () => {
|
|
103
|
+
if (!sessionId) return
|
|
104
|
+
try {
|
|
105
|
+
const msgs = await fetchMessages(sessionId)
|
|
106
|
+
if (msgs.length > messagesLenRef.current) {
|
|
107
|
+
const newMsgs = msgs.slice(messagesLenRef.current)
|
|
108
|
+
setMessages(msgs)
|
|
109
|
+
if (ttsEnabledRef.current && typeof document !== 'undefined' && document.visibilityState === 'visible') {
|
|
110
|
+
const latestAssistant = [...newMsgs].reverse().find((m) => {
|
|
111
|
+
if (m.role !== 'assistant') return false
|
|
112
|
+
const isHeartbeat = m.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(m.text || '')
|
|
113
|
+
return !isHeartbeat && !!m.text?.trim()
|
|
114
|
+
})
|
|
115
|
+
if (latestAssistant?.text) {
|
|
116
|
+
void speak(latestAssistant.text)
|
|
110
117
|
}
|
|
111
118
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
}
|
|
120
|
+
if (isServerActiveRef.current) await loadSessions()
|
|
121
|
+
} catch (err) { console.error('Failed to refresh messages:', err) }
|
|
122
|
+
}, [sessionId])
|
|
123
|
+
|
|
124
|
+
// Subscribe to WS messages for this session — always subscribe when session exists,
|
|
125
|
+
// only enable fallback polling when actively needed
|
|
126
|
+
useWs(
|
|
127
|
+
sessionId ? `messages:${sessionId}` : '',
|
|
128
|
+
refreshMessages,
|
|
129
|
+
shouldPollMessages ? 2000 : undefined,
|
|
130
|
+
)
|
|
120
131
|
|
|
121
132
|
// When server-active flag drops, stop the streaming indicator
|
|
122
133
|
useEffect(() => {
|
|
@@ -136,14 +147,17 @@ export function ChatArea() {
|
|
|
136
147
|
|
|
137
148
|
// Poll browser status while session has browser tools
|
|
138
149
|
const hasBrowserTool = session?.tools?.includes('browser')
|
|
139
|
-
|
|
150
|
+
const checkBrowserStatus = useCallback(() => {
|
|
140
151
|
if (!sessionId || !hasBrowserTool) return
|
|
141
|
-
|
|
142
|
-
checkBrowser(sessionId).then((r) => setBrowserActive(r.active)).catch(() => {})
|
|
143
|
-
}, 5000)
|
|
144
|
-
return () => clearInterval(interval)
|
|
152
|
+
checkBrowser(sessionId).then((r) => setBrowserActive(r.active)).catch(() => {})
|
|
145
153
|
}, [sessionId, hasBrowserTool])
|
|
146
154
|
|
|
155
|
+
useWs(
|
|
156
|
+
hasBrowserTool && sessionId ? `browser:${sessionId}` : '',
|
|
157
|
+
checkBrowserStatus,
|
|
158
|
+
hasBrowserTool ? 5000 : undefined,
|
|
159
|
+
)
|
|
160
|
+
|
|
147
161
|
const handleStopBrowser = useCallback(async () => {
|
|
148
162
|
if (!sessionId) return
|
|
149
163
|
await stopBrowser(sessionId)
|
|
@@ -3,54 +3,16 @@
|
|
|
3
3
|
import { useState, useRef, useEffect } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { api } from '@/lib/api-client'
|
|
6
|
+
import { AVAILABLE_TOOLS, PLATFORM_TOOLS, TOOL_LABELS } from '@/lib/tool-definitions'
|
|
7
|
+
import type { ToolDefinition } from '@/lib/tool-definitions'
|
|
6
8
|
import type { Session } from '@/types'
|
|
7
9
|
|
|
8
|
-
const TOOL_GROUPS: { label: string; tools:
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
tools: {
|
|
12
|
-
shell: 'Shell',
|
|
13
|
-
files: 'Files',
|
|
14
|
-
edit_file: 'Edit File',
|
|
15
|
-
process: 'Process',
|
|
16
|
-
web_search: 'Web Search',
|
|
17
|
-
web_fetch: 'Web Fetch',
|
|
18
|
-
browser: 'Browser',
|
|
19
|
-
memory: 'Memory',
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
label: 'Delegation',
|
|
24
|
-
tools: {
|
|
25
|
-
claude_code: 'Claude Code',
|
|
26
|
-
codex_cli: 'Codex CLI',
|
|
27
|
-
opencode_cli: 'OpenCode CLI',
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
label: 'Platform',
|
|
32
|
-
tools: {
|
|
33
|
-
orchestrator: 'Orchestrator',
|
|
34
|
-
manage_agents: 'Agents',
|
|
35
|
-
manage_tasks: 'Tasks',
|
|
36
|
-
manage_schedules: 'Schedules',
|
|
37
|
-
manage_skills: 'Skills',
|
|
38
|
-
manage_documents: 'Documents',
|
|
39
|
-
manage_webhooks: 'Webhooks',
|
|
40
|
-
manage_connectors: 'Connectors',
|
|
41
|
-
manage_sessions: 'Sessions',
|
|
42
|
-
manage_secrets: 'Secrets',
|
|
43
|
-
},
|
|
44
|
-
},
|
|
10
|
+
const TOOL_GROUPS: { label: string; tools: ToolDefinition[] }[] = [
|
|
11
|
+
{ label: 'Tools', tools: AVAILABLE_TOOLS },
|
|
12
|
+
{ label: 'Platform', tools: PLATFORM_TOOLS },
|
|
45
13
|
]
|
|
46
14
|
|
|
47
|
-
|
|
48
|
-
const ALL_TOOLS: Record<string, string> = {}
|
|
49
|
-
for (const g of TOOL_GROUPS) Object.assign(ALL_TOOLS, g.tools)
|
|
50
|
-
|
|
51
|
-
const TOOL_HINTS: Record<string, string> = {
|
|
52
|
-
orchestrator: 'Can delegate tasks to other agents',
|
|
53
|
-
}
|
|
15
|
+
const TOTAL_TOOL_COUNT = AVAILABLE_TOOLS.length + PLATFORM_TOOLS.length
|
|
54
16
|
|
|
55
17
|
interface Props {
|
|
56
18
|
session: Session
|
|
@@ -87,7 +49,7 @@ export function ChatToolToggles({ session }: Props) {
|
|
|
87
49
|
}
|
|
88
50
|
|
|
89
51
|
const enabledCount = sessionTools.length
|
|
90
|
-
const totalCount =
|
|
52
|
+
const totalCount = TOTAL_TOOL_COUNT
|
|
91
53
|
|
|
92
54
|
return (
|
|
93
55
|
<div className="relative" ref={ref}>
|
|
@@ -111,12 +73,12 @@ export function ChatToolToggles({ session }: Props) {
|
|
|
111
73
|
{TOOL_GROUPS.map((group, gi) => (
|
|
112
74
|
<div key={group.label} className={`px-3 pb-1 ${gi === 0 ? 'pt-3' : 'pt-1 border-t border-white/[0.04]'}`}>
|
|
113
75
|
<p className="text-[10px] font-600 text-text-3/60 uppercase tracking-wider mb-2">{group.label}</p>
|
|
114
|
-
{
|
|
115
|
-
const enabled = sessionTools.includes(
|
|
76
|
+
{group.tools.map((tool) => {
|
|
77
|
+
const enabled = sessionTools.includes(tool.id)
|
|
116
78
|
return (
|
|
117
|
-
<label key={
|
|
79
|
+
<label key={tool.id} className="flex items-center gap-2.5 py-1.5 cursor-pointer">
|
|
118
80
|
<div
|
|
119
|
-
onClick={() => toggleTool(
|
|
81
|
+
onClick={() => toggleTool(tool.id)}
|
|
120
82
|
className={`w-8 h-[18px] rounded-full transition-all duration-200 relative cursor-pointer shrink-0
|
|
121
83
|
${enabled ? 'bg-[#6366F1]' : 'bg-white/[0.12]'}`}
|
|
122
84
|
>
|
|
@@ -124,10 +86,7 @@ export function ChatToolToggles({ session }: Props) {
|
|
|
124
86
|
${enabled ? 'left-[16px]' : 'left-[2px]'}`} />
|
|
125
87
|
</div>
|
|
126
88
|
<span className={`text-[12px] ${enabled ? 'text-text-2' : 'text-text-3/70'}`}>
|
|
127
|
-
{label}
|
|
128
|
-
{TOOL_HINTS[toolId] && (
|
|
129
|
-
<span className="ml-2 text-[10px] text-text-3/70 font-400">{TOOL_HINTS[toolId]}</span>
|
|
130
|
-
)}
|
|
89
|
+
{tool.label}
|
|
131
90
|
</span>
|
|
132
91
|
</label>
|
|
133
92
|
)
|
|
@@ -168,6 +168,115 @@ function heartbeatSummary(text: string): string {
|
|
|
168
168
|
return clean.length > 180 ? `${clean.slice(0, 180)}...` : clean
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
const IMAGE_ATTACH_RE = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$/i
|
|
172
|
+
const PREVIEWABLE_ATTACH_RE = /\.(html?|svg)$/i
|
|
173
|
+
const FILE_TYPE_COLORS: Record<string, string> = {
|
|
174
|
+
html: 'text-orange-400', htm: 'text-orange-400', svg: 'text-emerald-400',
|
|
175
|
+
js: 'text-yellow-400', jsx: 'text-yellow-400', ts: 'text-blue-400', tsx: 'text-blue-400',
|
|
176
|
+
py: 'text-green-400', json: 'text-amber-300', css: 'text-purple-400', scss: 'text-pink-400',
|
|
177
|
+
md: 'text-text-2', txt: 'text-text-3', pdf: 'text-red-400',
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parseAttachmentUrl(filePath?: string, fileUrl?: string) {
|
|
181
|
+
const url = fileUrl || (filePath ? `/api/uploads/${filePath.split('/').pop()}` : '')
|
|
182
|
+
const rawName = filePath?.split('/').pop() || fileUrl?.split('/').pop() || 'file'
|
|
183
|
+
const filename = rawName.replace(/^[a-f0-9]+-/, '').split('?')[0]
|
|
184
|
+
return { url, filename }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function AttachmentChip({ url, filename, isUserMsg }: { url: string; filename: string; isUserMsg?: boolean }) {
|
|
188
|
+
const isImage = IMAGE_ATTACH_RE.test(filename)
|
|
189
|
+
if (isImage) {
|
|
190
|
+
return (
|
|
191
|
+
<img src={url} alt="Attached" className="max-w-[240px] rounded-[12px] mb-2 border border-white/10"
|
|
192
|
+
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
|
|
193
|
+
)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const ext = filename.split('.').pop()?.toLowerCase() || ''
|
|
197
|
+
const colorClass = FILE_TYPE_COLORS[ext] || 'text-text-3'
|
|
198
|
+
const isPreviewable = PREVIEWABLE_ATTACH_RE.test(filename)
|
|
199
|
+
|
|
200
|
+
// Solid bg so chip is readable on both user (purple) and assistant bubbles
|
|
201
|
+
const chipBg = isUserMsg
|
|
202
|
+
? 'bg-[rgba(0,0,0,0.25)] border-white/[0.12]'
|
|
203
|
+
: 'bg-[rgba(255,255,255,0.04)] border-white/[0.08]'
|
|
204
|
+
const iconBg = isUserMsg ? 'bg-white/[0.12]' : 'bg-white/[0.05]'
|
|
205
|
+
const btnBg = isUserMsg
|
|
206
|
+
? 'bg-white/[0.12] hover:bg-white/[0.18] text-white/80'
|
|
207
|
+
: 'bg-white/[0.06] hover:bg-white/[0.10] text-text-3'
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<div className={`flex items-center gap-3 px-4 py-2.5 mb-2 rounded-[12px] border ${chipBg}`}>
|
|
211
|
+
<div className={`flex items-center justify-center w-8 h-8 rounded-[8px] shrink-0 ${iconBg} ${colorClass}`}>
|
|
212
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
213
|
+
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
214
|
+
<polyline points="14 2 14 8 20 8" />
|
|
215
|
+
</svg>
|
|
216
|
+
</div>
|
|
217
|
+
<div className="flex flex-col flex-1 min-w-0">
|
|
218
|
+
<span className={`text-[13px] font-500 truncate ${isUserMsg ? 'text-white' : 'text-text'}`}>{filename}</span>
|
|
219
|
+
<span className={`text-[11px] uppercase tracking-wide ${isUserMsg ? 'text-white/50' : 'text-text-3/70'}`}>{ext || 'file'}</span>
|
|
220
|
+
</div>
|
|
221
|
+
{isPreviewable && (
|
|
222
|
+
<a href={url} target="_blank" rel="noopener noreferrer"
|
|
223
|
+
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 ${
|
|
224
|
+
isUserMsg ? 'bg-white/[0.15] hover:bg-white/[0.22] text-white' : 'bg-accent-soft hover:bg-accent-soft/80 text-accent-bright'
|
|
225
|
+
}`}
|
|
226
|
+
title="Preview in new tab">
|
|
227
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
228
|
+
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
229
|
+
<circle cx="12" cy="12" r="3" />
|
|
230
|
+
</svg>
|
|
231
|
+
Preview
|
|
232
|
+
</a>
|
|
233
|
+
)}
|
|
234
|
+
<a href={url} download={filename}
|
|
235
|
+
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] text-[11px] font-600 no-underline transition-colors shrink-0 ${btnBg}`}>
|
|
236
|
+
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
237
|
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
238
|
+
<polyline points="7 10 12 15 17 10" />
|
|
239
|
+
<line x1="12" y1="15" x2="12" y2="3" />
|
|
240
|
+
</svg>
|
|
241
|
+
Download
|
|
242
|
+
</a>
|
|
243
|
+
</div>
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function renderAttachments(message: Message) {
|
|
248
|
+
const isUser = message.role === 'user'
|
|
249
|
+
const seen = new Set<string>()
|
|
250
|
+
const chips: { url: string; filename: string }[] = []
|
|
251
|
+
|
|
252
|
+
// Primary attachment
|
|
253
|
+
if (message.imagePath || message.imageUrl) {
|
|
254
|
+
const primary = parseAttachmentUrl(message.imagePath, message.imageUrl)
|
|
255
|
+
if (primary.url) {
|
|
256
|
+
seen.add(primary.url)
|
|
257
|
+
chips.push(primary)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Additional attached files
|
|
262
|
+
if (message.attachedFiles?.length) {
|
|
263
|
+
for (const fp of message.attachedFiles) {
|
|
264
|
+
const att = parseAttachmentUrl(fp)
|
|
265
|
+
if (att.url && !seen.has(att.url)) {
|
|
266
|
+
seen.add(att.url)
|
|
267
|
+
chips.push(att)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!chips.length) return null
|
|
273
|
+
return (
|
|
274
|
+
<div className="flex flex-col">
|
|
275
|
+
{chips.map((c) => <AttachmentChip key={c.url} url={c.url} filename={c.filename} isUserMsg={isUser} />)}
|
|
276
|
+
</div>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
171
280
|
interface Props {
|
|
172
281
|
message: Message
|
|
173
282
|
assistantName?: string
|
|
@@ -240,48 +349,7 @@ export const MessageBubble = memo(function MessageBubble({ message, assistantNam
|
|
|
240
349
|
|
|
241
350
|
{/* Message bubble */}
|
|
242
351
|
<div className={`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'}`}>
|
|
243
|
-
{(message
|
|
244
|
-
const url = message.imageUrl || `/api/uploads/${message.imagePath?.split('/').pop()}`
|
|
245
|
-
const rawName = message.imagePath?.split('/').pop() || message.imageUrl?.split('/').pop() || 'file'
|
|
246
|
-
const filename = rawName.replace(/^[a-f0-9]+-/, '').split('?')[0]
|
|
247
|
-
const isImage = /\.(png|jpg|jpeg|gif|webp|svg|bmp|ico)$/i.test(filename)
|
|
248
|
-
if (isImage) {
|
|
249
|
-
return (
|
|
250
|
-
<img src={url} alt="Attached" className="max-w-[240px] rounded-[12px] mb-3 border border-white/10"
|
|
251
|
-
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }} />
|
|
252
|
-
)
|
|
253
|
-
}
|
|
254
|
-
const isPreviewable = /\.(html?|svg)$/i.test(filename)
|
|
255
|
-
return (
|
|
256
|
-
<div className="flex items-center gap-3 px-4 py-3 mb-3 rounded-[12px] border border-white/10 bg-white/[0.03]">
|
|
257
|
-
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3 shrink-0">
|
|
258
|
-
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
|
|
259
|
-
<polyline points="14 2 14 8 20 8" />
|
|
260
|
-
</svg>
|
|
261
|
-
<span className="text-[13px] text-text-2 font-500 truncate flex-1">{filename}</span>
|
|
262
|
-
{isPreviewable && (
|
|
263
|
-
<a href={url} target="_blank" rel="noopener noreferrer"
|
|
264
|
-
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-accent-soft hover:bg-accent-soft/80 text-accent-bright text-[11px] font-600 no-underline transition-colors shrink-0"
|
|
265
|
-
title="Preview in new tab">
|
|
266
|
-
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
267
|
-
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
|
268
|
-
<circle cx="12" cy="12" r="3" />
|
|
269
|
-
</svg>
|
|
270
|
-
Preview
|
|
271
|
-
</a>
|
|
272
|
-
)}
|
|
273
|
-
<a href={url} download={filename}
|
|
274
|
-
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.06] hover:bg-white/[0.10] text-text-3 text-[11px] font-600 no-underline transition-colors shrink-0">
|
|
275
|
-
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
276
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
|
277
|
-
<polyline points="7 10 12 15 17 10" />
|
|
278
|
-
<line x1="12" y1="15" x2="12" y2="3" />
|
|
279
|
-
</svg>
|
|
280
|
-
Download
|
|
281
|
-
</a>
|
|
282
|
-
</div>
|
|
283
|
-
)
|
|
284
|
-
})()}
|
|
352
|
+
{renderAttachments(message)}
|
|
285
353
|
|
|
286
354
|
{isHeartbeat ? (
|
|
287
355
|
<div className="flex flex-col gap-2">
|