@swarmclawai/swarmclaw 0.3.1 → 0.4.5
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 +33 -13
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +10 -0
- package/package.json +4 -1
- package/src/app/api/agents/[id]/route.ts +20 -18
- package/src/app/api/agents/[id]/thread/route.ts +4 -3
- package/src/app/api/agents/route.ts +8 -3
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +14 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +12 -4
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +5 -3
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/knowledge/[id]/route.ts +5 -4
- package/src/app/api/knowledge/upload/route.ts +2 -2
- package/src/app/api/mcp-servers/[id]/route.ts +11 -14
- package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
- package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
- package/src/app/api/mcp-servers/route.ts +5 -3
- package/src/app/api/memory/[id]/route.ts +9 -8
- package/src/app/api/memory/route.ts +2 -2
- package/src/app/api/memory-images/[filename]/route.ts +2 -1
- package/src/app/api/openclaw/directory/route.ts +26 -0
- package/src/app/api/openclaw/discover/route.ts +61 -0
- package/src/app/api/openclaw/sync/route.ts +30 -0
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/orchestrator/run/route.ts +2 -2
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/projects/[id]/route.ts +55 -0
- package/src/app/api/projects/route.ts +27 -0
- package/src/app/api/providers/[id]/models/route.ts +2 -1
- package/src/app/api/providers/[id]/route.ts +13 -12
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +7 -3
- package/src/app/api/schedules/[id]/route.ts +16 -15
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +8 -3
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +5 -3
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/[id]/clear/route.ts +2 -1
- package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
- package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
- package/src/app/api/sessions/[id]/messages/route.ts +2 -1
- package/src/app/api/sessions/[id]/retry/route.ts +2 -1
- package/src/app/api/sessions/[id]/route.ts +2 -1
- package/src/app/api/sessions/route.ts +11 -4
- 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/[id]/route.ts +23 -21
- package/src/app/api/skills/import/route.ts +2 -2
- package/src/app/api/skills/route.ts +5 -3
- package/src/app/api/tasks/[id]/approve/route.ts +74 -0
- package/src/app/api/tasks/[id]/route.ts +9 -5
- package/src/app/api/tasks/route.ts +5 -2
- package/src/app/api/tts/stream/route.ts +48 -0
- package/src/app/api/upload/route.ts +2 -2
- package/src/app/api/uploads/[filename]/route.ts +4 -1
- 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 +31 -32
- package/src/app/api/webhooks/route.ts +5 -3
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +11 -26
- package/src/cli/index.js +28 -9
- package/src/cli/index.ts +45 -2
- package/src/cli/spec.js +2 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +166 -81
- package/src/components/chat/chat-area.tsx +71 -34
- package/src/components/chat/chat-header.tsx +141 -29
- 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 +50 -6
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +9 -10
- package/src/components/connectors/connector-sheet.tsx +55 -36
- 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 +133 -90
- 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/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- 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 +9 -4
- 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 +14 -15
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/sessions/session-list.tsx +7 -7
- package/src/components/shared/connector-platform-icon.tsx +26 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +8 -40
- package/src/components/shared/settings/section-orchestrator.tsx +9 -11
- package/src/components/shared/settings/section-web-search.tsx +56 -0
- package/src/components/shared/settings/settings-page.tsx +73 -0
- package/src/components/skills/skill-list.tsx +262 -35
- 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 +8 -7
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-continuous-speech.ts +144 -0
- package/src/hooks/use-view-router.ts +52 -0
- package/src/hooks/use-voice-conversation.ts +80 -0
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/id.ts +6 -0
- package/src/lib/projects.ts +13 -0
- package/src/lib/provider-sets.ts +5 -0
- package/src/lib/providers/anthropic.ts +15 -2
- package/src/lib/providers/index.ts +8 -0
- package/src/lib/providers/ollama.ts +10 -2
- package/src/lib/providers/openai.ts +42 -13
- package/src/lib/providers/openclaw.ts +11 -0
- package/src/lib/server/api-routes.test.ts +5 -6
- package/src/lib/server/build-llm.ts +17 -4
- package/src/lib/server/chat-execution.ts +57 -8
- package/src/lib/server/collection-helpers.ts +54 -0
- package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
- package/src/lib/server/connectors/bluebubbles.ts +357 -0
- package/src/lib/server/connectors/connector-routing.test.ts +1 -1
- package/src/lib/server/connectors/googlechat.ts +46 -7
- package/src/lib/server/connectors/manager.ts +401 -6
- package/src/lib/server/connectors/media.ts +2 -2
- package/src/lib/server/connectors/openclaw.ts +64 -0
- package/src/lib/server/connectors/pairing.test.ts +99 -0
- package/src/lib/server/connectors/pairing.ts +256 -0
- package/src/lib/server/connectors/signal.ts +1 -0
- package/src/lib/server/connectors/teams.ts +5 -5
- package/src/lib/server/connectors/types.ts +10 -0
- 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/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +67 -8
- package/src/lib/server/memory-db.ts +6 -6
- package/src/lib/server/openclaw-approvals.ts +105 -0
- package/src/lib/server/openclaw-sync.ts +496 -0
- package/src/lib/server/orchestrator-lg.ts +422 -20
- package/src/lib/server/orchestrator.ts +29 -9
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +39 -13
- package/src/lib/server/scheduler.ts +2 -2
- package/src/lib/server/session-mailbox.ts +2 -2
- package/src/lib/server/session-run-manager.ts +8 -3
- package/src/lib/server/session-tools/connector.ts +51 -4
- package/src/lib/server/session-tools/crud.ts +3 -3
- package/src/lib/server/session-tools/delegate.ts +5 -5
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +4 -0
- package/src/lib/server/session-tools/memory.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
- package/src/lib/server/session-tools/sandbox.ts +197 -0
- package/src/lib/server/session-tools/search-providers.ts +270 -0
- package/src/lib/server/session-tools/session-info.ts +2 -2
- package/src/lib/server/session-tools/web.ts +47 -66
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +36 -7
- package/src/lib/server/stream-agent-chat.ts +106 -22
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- 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 +44 -0
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/view-routes.ts +28 -0
- package/src/lib/ws-client.ts +124 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +42 -14
- package/src/types/index.ts +34 -2
- 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
|
@@ -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'
|
|
@@ -11,13 +12,15 @@ import { ChatHeader } from './chat-header'
|
|
|
11
12
|
import { DevServerBar } from './dev-server-bar'
|
|
12
13
|
import { MessageList } from './message-list'
|
|
13
14
|
import { SessionDebugPanel } from './session-debug-panel'
|
|
15
|
+
import { VoiceOverlay } from './voice-overlay'
|
|
16
|
+
import { useVoiceConversation } from '@/hooks/use-voice-conversation'
|
|
14
17
|
import { ChatInput } from '@/components/input/chat-input'
|
|
15
18
|
import { Dropdown, DropdownItem } from '@/components/shared/dropdown'
|
|
16
19
|
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
17
20
|
import { speak } from '@/lib/tts'
|
|
18
21
|
|
|
19
22
|
const PROMPT_SUGGESTIONS = [
|
|
20
|
-
{ text: '
|
|
23
|
+
{ text: 'What can you help me with?', icon: 'book', gradient: 'from-[#6366F1]/10 to-[#818CF8]/5' },
|
|
21
24
|
{ text: 'Help me set up a new connector', icon: 'link', gradient: 'from-[#EC4899]/10 to-[#F472B6]/5' },
|
|
22
25
|
{ text: 'Create a new agent for me', icon: 'bot', gradient: 'from-[#34D399]/10 to-[#6EE7B7]/5' },
|
|
23
26
|
{ text: 'Schedule a recurring task', icon: 'check', gradient: 'from-[#F59E0B]/10 to-[#FBBF24]/5' },
|
|
@@ -42,6 +45,12 @@ export function ChatArea() {
|
|
|
42
45
|
const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
|
|
43
46
|
const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
|
|
44
47
|
|
|
48
|
+
const voice = useVoiceConversation()
|
|
49
|
+
const handleVoiceToggle = useCallback(() => {
|
|
50
|
+
if (voice.active) voice.stop()
|
|
51
|
+
else voice.start()
|
|
52
|
+
}, [voice])
|
|
53
|
+
|
|
45
54
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
46
55
|
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
47
56
|
const [confirmClear, setConfirmClear] = useState(false)
|
|
@@ -90,33 +99,43 @@ export function ChatArea() {
|
|
|
90
99
|
const isOrchestrated = session?.sessionType === 'orchestrated'
|
|
91
100
|
const isServerActive = session?.active === true
|
|
92
101
|
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
|
-
|
|
102
|
+
const shouldPollMessages = !!sessionId && (isOrchestrated || isServerActive || isOngoingMonitored)
|
|
103
|
+
const messagesLenRef = useRef(messages.length)
|
|
104
|
+
messagesLenRef.current = messages.length
|
|
105
|
+
const isServerActiveRef = useRef(isServerActive)
|
|
106
|
+
isServerActiveRef.current = isServerActive
|
|
107
|
+
const ttsEnabledRef = useRef(ttsEnabled)
|
|
108
|
+
ttsEnabledRef.current = ttsEnabled
|
|
109
|
+
|
|
110
|
+
const refreshMessages = useCallback(async () => {
|
|
111
|
+
if (!sessionId) return
|
|
112
|
+
try {
|
|
113
|
+
const msgs = await fetchMessages(sessionId)
|
|
114
|
+
if (msgs.length > messagesLenRef.current) {
|
|
115
|
+
const newMsgs = msgs.slice(messagesLenRef.current)
|
|
116
|
+
setMessages(msgs)
|
|
117
|
+
if (ttsEnabledRef.current && typeof document !== 'undefined' && document.visibilityState === 'visible') {
|
|
118
|
+
const latestAssistant = [...newMsgs].reverse().find((m) => {
|
|
119
|
+
if (m.role !== 'assistant') return false
|
|
120
|
+
const isHeartbeat = m.kind === 'heartbeat' || /^\s*HEARTBEAT_OK\b/i.test(m.text || '')
|
|
121
|
+
return !isHeartbeat && !!m.text?.trim()
|
|
122
|
+
})
|
|
123
|
+
if (latestAssistant?.text) {
|
|
124
|
+
void speak(latestAssistant.text)
|
|
110
125
|
}
|
|
111
126
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
}
|
|
128
|
+
if (isServerActiveRef.current) await loadSessions()
|
|
129
|
+
} catch (err) { console.error('Failed to refresh messages:', err) }
|
|
130
|
+
}, [sessionId])
|
|
131
|
+
|
|
132
|
+
// Subscribe to WS messages for this session — always subscribe when session exists,
|
|
133
|
+
// only enable fallback polling when actively needed
|
|
134
|
+
useWs(
|
|
135
|
+
sessionId ? `messages:${sessionId}` : '',
|
|
136
|
+
refreshMessages,
|
|
137
|
+
shouldPollMessages ? 2000 : undefined,
|
|
138
|
+
)
|
|
120
139
|
|
|
121
140
|
// When server-active flag drops, stop the streaming indicator
|
|
122
141
|
useEffect(() => {
|
|
@@ -136,14 +155,17 @@ export function ChatArea() {
|
|
|
136
155
|
|
|
137
156
|
// Poll browser status while session has browser tools
|
|
138
157
|
const hasBrowserTool = session?.tools?.includes('browser')
|
|
139
|
-
|
|
158
|
+
const checkBrowserStatus = useCallback(() => {
|
|
140
159
|
if (!sessionId || !hasBrowserTool) return
|
|
141
|
-
|
|
142
|
-
checkBrowser(sessionId).then((r) => setBrowserActive(r.active)).catch(() => {})
|
|
143
|
-
}, 5000)
|
|
144
|
-
return () => clearInterval(interval)
|
|
160
|
+
checkBrowser(sessionId).then((r) => setBrowserActive(r.active)).catch(() => {})
|
|
145
161
|
}, [sessionId, hasBrowserTool])
|
|
146
162
|
|
|
163
|
+
useWs(
|
|
164
|
+
hasBrowserTool && sessionId ? `browser:${sessionId}` : '',
|
|
165
|
+
checkBrowserStatus,
|
|
166
|
+
hasBrowserTool ? 5000 : undefined,
|
|
167
|
+
)
|
|
168
|
+
|
|
147
169
|
const handleStopBrowser = useCallback(async () => {
|
|
148
170
|
if (!sessionId) return
|
|
149
171
|
await stopBrowser(sessionId)
|
|
@@ -237,6 +259,9 @@ export function ChatArea() {
|
|
|
237
259
|
onBack={handleBack}
|
|
238
260
|
browserActive={browserActive}
|
|
239
261
|
onStopBrowser={handleStopBrowser}
|
|
262
|
+
voiceActive={voice.active}
|
|
263
|
+
voiceSupported={voice.supported}
|
|
264
|
+
onVoiceToggle={handleVoiceToggle}
|
|
240
265
|
/>
|
|
241
266
|
)}
|
|
242
267
|
{!isDesktop && (
|
|
@@ -248,6 +273,9 @@ export function ChatArea() {
|
|
|
248
273
|
mobile
|
|
249
274
|
browserActive={browserActive}
|
|
250
275
|
onStopBrowser={handleStopBrowser}
|
|
276
|
+
voiceActive={voice.active}
|
|
277
|
+
voiceSupported={voice.supported}
|
|
278
|
+
onVoiceToggle={handleVoiceToggle}
|
|
251
279
|
/>
|
|
252
280
|
)}
|
|
253
281
|
<DevServerBar status={devServerStatus} onStop={handleStopDevServer} />
|
|
@@ -306,6 +334,15 @@ export function ChatArea() {
|
|
|
306
334
|
<MessageList messages={messages} streaming={streamingForThisSession} />
|
|
307
335
|
)}
|
|
308
336
|
|
|
337
|
+
{voice.active && (
|
|
338
|
+
<VoiceOverlay
|
|
339
|
+
state={voice.state}
|
|
340
|
+
interimText={voice.interimText}
|
|
341
|
+
transcript={voice.transcript}
|
|
342
|
+
onStop={voice.stop}
|
|
343
|
+
/>
|
|
344
|
+
)}
|
|
345
|
+
|
|
309
346
|
<SessionDebugPanel
|
|
310
347
|
messages={messages}
|
|
311
348
|
open={debugOpen}
|
|
@@ -334,7 +371,7 @@ export function ChatArea() {
|
|
|
334
371
|
)}
|
|
335
372
|
{!isMainChat && (
|
|
336
373
|
<DropdownItem danger onClick={() => { setMenuOpen(false); setConfirmDelete(true) }}>
|
|
337
|
-
Delete
|
|
374
|
+
Delete Chat
|
|
338
375
|
</DropdownItem>
|
|
339
376
|
)}
|
|
340
377
|
</Dropdown>
|
|
@@ -342,7 +379,7 @@ export function ChatArea() {
|
|
|
342
379
|
<ConfirmDialog
|
|
343
380
|
open={confirmClear}
|
|
344
381
|
title="Clear History"
|
|
345
|
-
message="This will delete all messages in this
|
|
382
|
+
message="This will delete all messages in this chat. This cannot be undone."
|
|
346
383
|
confirmLabel="Clear"
|
|
347
384
|
danger
|
|
348
385
|
onConfirm={handleClear}
|
|
@@ -350,7 +387,7 @@ export function ChatArea() {
|
|
|
350
387
|
/>
|
|
351
388
|
<ConfirmDialog
|
|
352
389
|
open={confirmDelete}
|
|
353
|
-
title="Delete
|
|
390
|
+
title="Delete Chat"
|
|
354
391
|
message={`Delete "${session.name}"? This cannot be undone.`}
|
|
355
392
|
confirmLabel="Delete"
|
|
356
393
|
danger
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useEffect, useState, useMemo } from 'react'
|
|
3
|
+
import { useEffect, useState, useMemo, useRef } from 'react'
|
|
4
4
|
import type { Session } from '@/types'
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
6
|
import { useChatStore } from '@/stores/use-chat-store'
|
|
@@ -18,6 +18,16 @@ function shortPath(p: string): string {
|
|
|
18
18
|
return (p || '').replace(/^\/Users\/\w+/, '~')
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function formatDuration(sec: number): string {
|
|
22
|
+
if (sec >= 3600) {
|
|
23
|
+
const h = Math.floor(sec / 3600)
|
|
24
|
+
const m = Math.floor((sec % 3600) / 60)
|
|
25
|
+
return m > 0 ? `${h}h${m}m` : `${h}h`
|
|
26
|
+
}
|
|
27
|
+
if (sec >= 60) return `${Math.floor(sec / 60)}m`
|
|
28
|
+
return `${sec}s`
|
|
29
|
+
}
|
|
30
|
+
|
|
21
31
|
const PROVIDER_LABELS: Record<string, string> = {
|
|
22
32
|
'claude-cli': 'CLI',
|
|
23
33
|
openai: 'OpenAI',
|
|
@@ -34,9 +44,12 @@ interface Props {
|
|
|
34
44
|
mobile?: boolean
|
|
35
45
|
browserActive?: boolean
|
|
36
46
|
onStopBrowser?: () => void
|
|
47
|
+
onVoiceToggle?: () => void
|
|
48
|
+
voiceActive?: boolean
|
|
49
|
+
voiceSupported?: boolean
|
|
37
50
|
}
|
|
38
51
|
|
|
39
|
-
export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser }: Props) {
|
|
52
|
+
export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, mobile, browserActive, onStopBrowser, onVoiceToggle, voiceActive, voiceSupported }: Props) {
|
|
40
53
|
const ttsEnabled = useChatStore((s) => s.ttsEnabled)
|
|
41
54
|
const toggleTts = useChatStore((s) => s.toggleTts)
|
|
42
55
|
const debugOpen = useChatStore((s) => s.debugOpen)
|
|
@@ -49,6 +62,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
49
62
|
const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
|
|
50
63
|
const appSettings = useAppStore((s) => s.appSettings)
|
|
51
64
|
const loadSessions = useAppStore((s) => s.loadSessions)
|
|
65
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
52
66
|
const connectors = useAppStore((s) => s.connectors)
|
|
53
67
|
const loadConnectors = useAppStore((s) => s.loadConnectors)
|
|
54
68
|
const providerLabel = PROVIDER_LABELS[session.provider] || session.provider
|
|
@@ -58,6 +72,8 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
58
72
|
const modelName = session.model || agent?.model || ''
|
|
59
73
|
const [copied, setCopied] = useState(false)
|
|
60
74
|
const [heartbeatSaving, setHeartbeatSaving] = useState(false)
|
|
75
|
+
const [hbDropdownOpen, setHbDropdownOpen] = useState(false)
|
|
76
|
+
const hbDropdownRef = useRef<HTMLDivElement>(null)
|
|
61
77
|
const [mainLoopSaving, setMainLoopSaving] = useState(false)
|
|
62
78
|
const [mainLoopError, setMainLoopError] = useState('')
|
|
63
79
|
const [mainLoopNotice, setMainLoopNotice] = useState('')
|
|
@@ -102,11 +118,53 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
102
118
|
setTimeout(() => setCopied(false), 2000)
|
|
103
119
|
}
|
|
104
120
|
|
|
105
|
-
const heartbeatEnabled = session.heartbeatEnabled !== false
|
|
106
121
|
const heartbeatSupported = (session.tools?.length ?? 0) > 0
|
|
107
122
|
const loopIsOngoing = appSettings.loopMode === 'ongoing'
|
|
108
|
-
const
|
|
109
|
-
|
|
123
|
+
const { heartbeatEnabled, heartbeatIntervalSec, heartbeatExplicitOptIn } = useMemo(() => {
|
|
124
|
+
// Resolve through the same cascade as the backend: settings → agent → session
|
|
125
|
+
const parseDur = (v: unknown): number | null => {
|
|
126
|
+
if (v === null || v === undefined) return null
|
|
127
|
+
if (typeof v === 'number') return Number.isFinite(v) ? Math.max(0, Math.min(86400, Math.trunc(v))) : null
|
|
128
|
+
if (typeof v !== 'string') return null
|
|
129
|
+
const t = v.trim().toLowerCase()
|
|
130
|
+
if (!t) return null
|
|
131
|
+
const n = Number(t)
|
|
132
|
+
if (Number.isFinite(n)) return Math.max(0, Math.min(86400, Math.trunc(n)))
|
|
133
|
+
const m = t.match(/^(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s?)?$/)
|
|
134
|
+
if (!m || (!m[1] && !m[2] && !m[3])) return null
|
|
135
|
+
const total = (m[1] ? parseInt(m[1]) * 3600 : 0) + (m[2] ? parseInt(m[2]) * 60 : 0) + (m[3] ? parseInt(m[3]) : 0)
|
|
136
|
+
return Math.max(0, Math.min(86400, total))
|
|
137
|
+
}
|
|
138
|
+
const resolveFrom = (obj: Record<string, any>): number | null => {
|
|
139
|
+
const dur = parseDur(obj.heartbeatInterval)
|
|
140
|
+
if (dur !== null) return dur
|
|
141
|
+
const sec = parseDur(obj.heartbeatIntervalSec)
|
|
142
|
+
if (sec !== null) return sec
|
|
143
|
+
return null
|
|
144
|
+
}
|
|
145
|
+
// Global defaults
|
|
146
|
+
let sec = resolveFrom(appSettings as Record<string, any>) ?? 1800
|
|
147
|
+
let enabled = sec > 0
|
|
148
|
+
let explicitOptIn = false
|
|
149
|
+
// Agent layer
|
|
150
|
+
if (agent) {
|
|
151
|
+
if (agent.heartbeatEnabled === false) enabled = false
|
|
152
|
+
if (agent.heartbeatEnabled === true) { enabled = true; explicitOptIn = true }
|
|
153
|
+
sec = resolveFrom(agent as Record<string, any>) ?? sec
|
|
154
|
+
}
|
|
155
|
+
// Session layer — only applies for non-agent chats (agent chats save directly to agent)
|
|
156
|
+
if (!agent) {
|
|
157
|
+
if (session.heartbeatEnabled === false) enabled = false
|
|
158
|
+
if (session.heartbeatEnabled === true) { enabled = true; explicitOptIn = true }
|
|
159
|
+
sec = resolveFrom(session as Record<string, any>) ?? sec
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
heartbeatEnabled: enabled && sec > 0,
|
|
163
|
+
heartbeatIntervalSec: sec,
|
|
164
|
+
heartbeatExplicitOptIn: explicitOptIn,
|
|
165
|
+
}
|
|
166
|
+
}, [appSettings, agent, session])
|
|
167
|
+
const heartbeatWillRun = heartbeatEnabled && (loopIsOngoing || heartbeatExplicitOptIn)
|
|
110
168
|
const isMainSession = session.name === '__main__'
|
|
111
169
|
const missionState = session.mainLoopState || {}
|
|
112
170
|
const missionPaused = missionState.paused === true
|
|
@@ -119,23 +177,40 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
119
177
|
if (!heartbeatSupported || heartbeatSaving) return
|
|
120
178
|
setHeartbeatSaving(true)
|
|
121
179
|
try {
|
|
122
|
-
|
|
123
|
-
|
|
180
|
+
const next = !heartbeatEnabled
|
|
181
|
+
if (session.agentId) {
|
|
182
|
+
await api('PUT', `/agents/${session.agentId}`, { heartbeatEnabled: next })
|
|
183
|
+
// Clear any stale session-level override so the agent value wins
|
|
184
|
+
await api('PUT', `/sessions/${session.id}`, { heartbeatEnabled: null })
|
|
185
|
+
await Promise.all([loadAgents(), loadSessions()])
|
|
186
|
+
} else {
|
|
187
|
+
await api('PUT', `/sessions/${session.id}`, { heartbeatEnabled: next })
|
|
188
|
+
await loadSessions()
|
|
189
|
+
}
|
|
124
190
|
} finally {
|
|
125
191
|
setHeartbeatSaving(false)
|
|
126
192
|
}
|
|
127
193
|
}
|
|
128
194
|
|
|
129
|
-
const
|
|
195
|
+
const handleSelectHeartbeatInterval = async (sec: number) => {
|
|
130
196
|
if (!heartbeatSupported || heartbeatSaving) return
|
|
131
|
-
|
|
132
|
-
const current = heartbeatIntervalSec
|
|
133
|
-
const idx = presets.indexOf(current)
|
|
134
|
-
const next = idx === -1 ? 120 : presets[(idx + 1) % presets.length]
|
|
197
|
+
setHbDropdownOpen(false)
|
|
135
198
|
setHeartbeatSaving(true)
|
|
136
199
|
try {
|
|
137
|
-
|
|
138
|
-
|
|
200
|
+
if (session.agentId) {
|
|
201
|
+
// Save to agent with both formats so the cascade resolves correctly
|
|
202
|
+
await api('PUT', `/agents/${session.agentId}`, {
|
|
203
|
+
heartbeatInterval: formatDuration(sec),
|
|
204
|
+
heartbeatIntervalSec: sec,
|
|
205
|
+
heartbeatEnabled: true,
|
|
206
|
+
})
|
|
207
|
+
// Clear stale session-level overrides
|
|
208
|
+
await api('PUT', `/sessions/${session.id}`, { heartbeatIntervalSec: null, heartbeatEnabled: null })
|
|
209
|
+
await Promise.all([loadAgents(), loadSessions()])
|
|
210
|
+
} else {
|
|
211
|
+
await api('PUT', `/sessions/${session.id}`, { heartbeatIntervalSec: sec, heartbeatEnabled: true })
|
|
212
|
+
await loadSessions()
|
|
213
|
+
}
|
|
139
214
|
} finally {
|
|
140
215
|
setHeartbeatSaving(false)
|
|
141
216
|
}
|
|
@@ -193,6 +268,15 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
193
268
|
void postMainLoopAction('clear_events')
|
|
194
269
|
}
|
|
195
270
|
|
|
271
|
+
useEffect(() => {
|
|
272
|
+
if (!hbDropdownOpen) return
|
|
273
|
+
const handler = (e: MouseEvent) => {
|
|
274
|
+
if (hbDropdownRef.current && !hbDropdownRef.current.contains(e.target as Node)) setHbDropdownOpen(false)
|
|
275
|
+
}
|
|
276
|
+
document.addEventListener('mousedown', handler)
|
|
277
|
+
return () => document.removeEventListener('mousedown', handler)
|
|
278
|
+
}, [hbDropdownOpen])
|
|
279
|
+
|
|
196
280
|
useEffect(() => {
|
|
197
281
|
if (session.name.startsWith('connector:')) {
|
|
198
282
|
void loadConnectors()
|
|
@@ -298,7 +382,16 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
298
382
|
<path d="M15.54 8.46a5 5 0 0 1 0 7.07" />
|
|
299
383
|
</svg>
|
|
300
384
|
</IconButton>
|
|
301
|
-
|
|
385
|
+
{voiceSupported && onVoiceToggle && (
|
|
386
|
+
<IconButton onClick={onVoiceToggle} active={voiceActive} aria-label="Toggle voice conversation">
|
|
387
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
388
|
+
<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z" />
|
|
389
|
+
<path d="M19 10v2a7 7 0 0 1-14 0v-2" />
|
|
390
|
+
<line x1="12" x2="12" y1="19" y2="22" />
|
|
391
|
+
</svg>
|
|
392
|
+
</IconButton>
|
|
393
|
+
)}
|
|
394
|
+
<IconButton onClick={(e) => { e.stopPropagation(); onMenuToggle() }} aria-label="Chat menu">
|
|
302
395
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
|
|
303
396
|
<circle cx="12" cy="6" r="1" />
|
|
304
397
|
<circle cx="12" cy="12" r="1" />
|
|
@@ -320,25 +413,44 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
320
413
|
onClick={handleToggleHeartbeat}
|
|
321
414
|
disabled={heartbeatSaving}
|
|
322
415
|
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] transition-colors cursor-pointer border-none
|
|
323
|
-
${
|
|
324
|
-
title={
|
|
416
|
+
${heartbeatWillRun ? 'bg-emerald-500/10 hover:bg-emerald-500/15 text-emerald-400' : 'bg-white/[0.04] hover:bg-white/[0.07] text-text-3'}`}
|
|
417
|
+
title={heartbeatWillRun ? 'Toggle heartbeat' : !heartbeatEnabled ? 'Heartbeat disabled — click to enable' : 'Heartbeat enabled but paused (bounded loop mode, no explicit opt-in)'}
|
|
325
418
|
>
|
|
326
|
-
<span className={`w-1.5 h-1.5 rounded-full ${
|
|
419
|
+
<span className={`w-1.5 h-1.5 rounded-full ${heartbeatWillRun ? 'bg-emerald-400' : 'bg-text-3/40'}`} />
|
|
327
420
|
<span className="text-[11px] font-600">
|
|
328
|
-
HB {
|
|
421
|
+
HB {heartbeatWillRun ? 'On' : 'Off'}
|
|
329
422
|
</span>
|
|
330
|
-
{!loopIsOngoing && (
|
|
423
|
+
{heartbeatEnabled && !loopIsOngoing && !heartbeatExplicitOptIn && (
|
|
331
424
|
<span className="text-[10px] text-text-3/50">(bounded)</span>
|
|
332
425
|
)}
|
|
333
426
|
</button>
|
|
334
|
-
<
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
427
|
+
<div className="relative" ref={hbDropdownRef}>
|
|
428
|
+
<button
|
|
429
|
+
onClick={() => setHbDropdownOpen((o) => !o)}
|
|
430
|
+
disabled={heartbeatSaving}
|
|
431
|
+
className="flex items-center gap-1 px-2.5 py-1 rounded-[8px] bg-white/[0.04] hover:bg-white/[0.07] text-text-3 transition-colors cursor-pointer border-none"
|
|
432
|
+
title="Set heartbeat interval"
|
|
433
|
+
>
|
|
434
|
+
<span className="text-[11px] font-600">{formatDuration(heartbeatIntervalSec)}</span>
|
|
435
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-text-3/50">
|
|
436
|
+
<polyline points="6 9 12 15 18 9" />
|
|
437
|
+
</svg>
|
|
438
|
+
</button>
|
|
439
|
+
{hbDropdownOpen && (
|
|
440
|
+
<div className="absolute top-full left-0 mt-1 py-1 rounded-[10px] border border-white/[0.06] bg-bg/95 backdrop-blur-md shadow-lg z-50 min-w-[80px]">
|
|
441
|
+
{[30, 60, 120, 300, 600, 1800, 3600].map((sec) => (
|
|
442
|
+
<button
|
|
443
|
+
key={sec}
|
|
444
|
+
onClick={() => handleSelectHeartbeatInterval(sec)}
|
|
445
|
+
className={`w-full text-left px-3 py-1.5 text-[11px] font-600 transition-colors cursor-pointer border-none
|
|
446
|
+
${sec === heartbeatIntervalSec ? 'bg-accent-soft text-accent-bright' : 'text-text-3 hover:bg-white/[0.06]'}`}
|
|
447
|
+
>
|
|
448
|
+
{formatDuration(sec)}
|
|
449
|
+
</button>
|
|
450
|
+
))}
|
|
451
|
+
</div>
|
|
452
|
+
)}
|
|
453
|
+
</div>
|
|
342
454
|
</>
|
|
343
455
|
)}
|
|
344
456
|
{isMainSession && (
|
|
@@ -467,7 +579,7 @@ export function ChatHeader({ session, streaming, onStop, onMenuToggle, onBack, m
|
|
|
467
579
|
<button
|
|
468
580
|
onClick={onStopBrowser}
|
|
469
581
|
className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-[#3B82F6]/10 hover:bg-[#F43F5E]/15 transition-colors cursor-pointer group"
|
|
470
|
-
title="Stop browser
|
|
582
|
+
title="Stop browser"
|
|
471
583
|
>
|
|
472
584
|
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" className="text-[#3B82F6] group-hover:text-[#F43F5E]">
|
|
473
585
|
<rect x="3" y="3" width="18" height="14" rx="2" />
|
|
@@ -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
|
)
|