@swarmclawai/swarmclaw 0.4.0 → 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 +13 -2
- package/next.config.ts +8 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +20 -21
- package/src/app/api/agents/[id]/thread/route.ts +2 -2
- package/src/app/api/agents/route.ts +3 -2
- package/src/app/api/clawhub/install/route.ts +2 -2
- package/src/app/api/connectors/[id]/route.ts +10 -3
- package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
- package/src/app/api/connectors/route.ts +6 -3
- package/src/app/api/credentials/[id]/route.ts +2 -1
- package/src/app/api/credentials/route.ts +2 -2
- package/src/app/api/documents/route.ts +2 -2
- package/src/app/api/files/serve/route.ts +8 -0
- 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 +2 -2
- 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/run/route.ts +2 -2
- 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 -15
- package/src/app/api/providers/route.ts +2 -2
- package/src/app/api/schedules/[id]/route.ts +16 -18
- package/src/app/api/schedules/[id]/run/route.ts +4 -3
- package/src/app/api/schedules/route.ts +2 -2
- package/src/app/api/secrets/[id]/route.ts +16 -17
- package/src/app/api/secrets/route.ts +2 -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 +2 -2
- 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 +2 -2
- package/src/app/api/tasks/[id]/approve/route.ts +2 -1
- package/src/app/api/tasks/[id]/route.ts +6 -5
- package/src/app/api/tasks/route.ts +2 -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/webhooks/[id]/route.ts +29 -31
- package/src/app/api/webhooks/route.ts +2 -2
- package/src/app/page.tsx +3 -24
- package/src/cli/index.js +28 -0
- package/src/cli/index.ts +1 -1
- package/src/cli/spec.js +2 -0
- package/src/components/agents/agent-list.tsx +3 -1
- package/src/components/agents/agent-sheet.tsx +116 -14
- package/src/components/chat/chat-area.tsx +27 -4
- package/src/components/chat/chat-header.tsx +141 -29
- package/src/components/chat/tool-call-bubble.tsx +9 -3
- package/src/components/chat/voice-overlay.tsx +80 -0
- package/src/components/connectors/connector-list.tsx +6 -2
- package/src/components/connectors/connector-sheet.tsx +31 -7
- package/src/components/layout/app-layout.tsx +47 -25
- package/src/components/projects/project-list.tsx +122 -0
- package/src/components/projects/project-sheet.tsx +135 -0
- package/src/components/schedules/schedule-list.tsx +3 -1
- package/src/components/sessions/new-session-sheet.tsx +6 -6
- 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 +4 -0
- package/src/components/shared/settings/section-heartbeat.tsx +1 -1
- package/src/components/shared/settings/section-orchestrator.tsx +1 -2
- 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 +2 -1
- package/src/components/tasks/task-list.tsx +5 -2
- 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/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 +14 -1
- package/src/lib/providers/index.ts +6 -0
- package/src/lib/providers/ollama.ts +9 -1
- package/src/lib/providers/openai.ts +9 -1
- 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 +38 -4
- 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 +392 -3
- 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/execution-log.ts +3 -3
- package/src/lib/server/heartbeat-service.ts +1 -1
- package/src/lib/server/knowledge-db.test.ts +2 -33
- package/src/lib/server/main-agent-loop.ts +6 -6
- 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 +30 -9
- package/src/lib/server/orchestrator.ts +4 -4
- package/src/lib/server/process-manager.ts +2 -2
- package/src/lib/server/queue.ts +22 -10
- 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 +2 -2
- 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 +3 -3
- package/src/lib/server/session-tools/file.ts +176 -3
- package/src/lib/server/session-tools/index.ts +2 -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 +33 -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.ts +12 -0
- package/src/lib/server/stream-agent-chat.ts +29 -0
- package/src/lib/server/task-result.test.ts +44 -0
- package/src/lib/server/task-result.ts +14 -0
- package/src/lib/tool-definitions.ts +5 -3
- package/src/lib/tts-stream.ts +130 -0
- package/src/lib/view-routes.ts +28 -0
- package/src/proxy.ts +3 -0
- package/src/stores/use-app-store.ts +28 -1
- package/src/stores/use-chat-store.ts +9 -1
- package/src/types/index.ts +27 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { createProject, updateProject, deleteProject } from '@/lib/projects'
|
|
6
|
+
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
7
|
+
import { toast } from 'sonner'
|
|
8
|
+
|
|
9
|
+
const PROJECT_COLORS = [
|
|
10
|
+
'#EF4444', '#F97316', '#EAB308', '#22C55E', '#06B6D4',
|
|
11
|
+
'#3B82F6', '#8B5CF6', '#EC4899', '#6B7280',
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
const inputClass = 'w-full px-3 py-2.5 rounded-lg bg-white/[0.06] border border-white/[0.06] text-[13px] text-text-1 placeholder:text-text-3/40 focus:outline-none focus:border-accent/40 transition-colors'
|
|
15
|
+
|
|
16
|
+
export function ProjectSheet() {
|
|
17
|
+
const open = useAppStore((s) => s.projectSheetOpen)
|
|
18
|
+
const setOpen = useAppStore((s) => s.setProjectSheetOpen)
|
|
19
|
+
const editingId = useAppStore((s) => s.editingProjectId)
|
|
20
|
+
const setEditingId = useAppStore((s) => s.setEditingProjectId)
|
|
21
|
+
const projects = useAppStore((s) => s.projects)
|
|
22
|
+
const loadProjects = useAppStore((s) => s.loadProjects)
|
|
23
|
+
|
|
24
|
+
const [name, setName] = useState('')
|
|
25
|
+
const [description, setDescription] = useState('')
|
|
26
|
+
const [color, setColor] = useState<string | undefined>(undefined)
|
|
27
|
+
|
|
28
|
+
const editing = editingId ? projects[editingId] : null
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (open) {
|
|
32
|
+
if (editing) {
|
|
33
|
+
setName(editing.name)
|
|
34
|
+
setDescription(editing.description)
|
|
35
|
+
setColor(editing.color)
|
|
36
|
+
} else {
|
|
37
|
+
setName('')
|
|
38
|
+
setDescription('')
|
|
39
|
+
setColor(PROJECT_COLORS[0])
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
43
|
+
}, [open, editingId])
|
|
44
|
+
|
|
45
|
+
const onClose = () => {
|
|
46
|
+
setOpen(false)
|
|
47
|
+
setEditingId(null)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handleSave = async () => {
|
|
51
|
+
const data = {
|
|
52
|
+
name: name.trim() || 'Unnamed Project',
|
|
53
|
+
description,
|
|
54
|
+
color,
|
|
55
|
+
}
|
|
56
|
+
if (editing) {
|
|
57
|
+
await updateProject(editing.id, data)
|
|
58
|
+
} else {
|
|
59
|
+
await createProject(data)
|
|
60
|
+
}
|
|
61
|
+
await loadProjects()
|
|
62
|
+
onClose()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleDelete = async () => {
|
|
66
|
+
if (editing) {
|
|
67
|
+
await deleteProject(editing.id)
|
|
68
|
+
await loadProjects()
|
|
69
|
+
onClose()
|
|
70
|
+
toast.success('Project deleted')
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<BottomSheet open={open} onClose={onClose}>
|
|
76
|
+
<h2 className="font-display text-[18px] font-700 text-text mb-6">{editing ? 'Edit Project' : 'New Project'}</h2>
|
|
77
|
+
<div className="mb-6">
|
|
78
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Name</label>
|
|
79
|
+
<input
|
|
80
|
+
type="text"
|
|
81
|
+
value={name}
|
|
82
|
+
onChange={(e) => setName(e.target.value)}
|
|
83
|
+
placeholder="e.g. Marketing Site"
|
|
84
|
+
className={inputClass}
|
|
85
|
+
style={{ fontFamily: 'inherit' }}
|
|
86
|
+
autoFocus
|
|
87
|
+
/>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div className="mb-6">
|
|
91
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Description</label>
|
|
92
|
+
<textarea
|
|
93
|
+
value={description}
|
|
94
|
+
onChange={(e) => setDescription(e.target.value)}
|
|
95
|
+
placeholder="What is this project about?"
|
|
96
|
+
className={inputClass + ' min-h-[80px] resize-y'}
|
|
97
|
+
style={{ fontFamily: 'inherit' }}
|
|
98
|
+
rows={3}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="mb-8">
|
|
103
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">Color</label>
|
|
104
|
+
<div className="flex items-center gap-2">
|
|
105
|
+
{PROJECT_COLORS.map((c) => (
|
|
106
|
+
<button
|
|
107
|
+
key={c}
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => setColor(c)}
|
|
110
|
+
className={`w-7 h-7 rounded-full transition-all ${color === c ? 'ring-2 ring-offset-2 ring-offset-surface ring-accent scale-110' : 'hover:scale-105'}`}
|
|
111
|
+
style={{ backgroundColor: c }}
|
|
112
|
+
/>
|
|
113
|
+
))}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="flex items-center gap-3">
|
|
118
|
+
<button
|
|
119
|
+
onClick={handleSave}
|
|
120
|
+
className="flex-1 py-2.5 rounded-lg bg-accent text-white text-[13px] font-600 hover:bg-accent-bright transition-colors"
|
|
121
|
+
>
|
|
122
|
+
{editing ? 'Update' : 'Create'} Project
|
|
123
|
+
</button>
|
|
124
|
+
{editing && (
|
|
125
|
+
<button
|
|
126
|
+
onClick={handleDelete}
|
|
127
|
+
className="px-4 py-2.5 rounded-lg bg-red-500/10 text-red-400 text-[13px] font-600 hover:bg-red-500/20 transition-colors"
|
|
128
|
+
>
|
|
129
|
+
Delete
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
</BottomSheet>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -12,6 +12,7 @@ export function ScheduleList({ inSidebar }: Props) {
|
|
|
12
12
|
const schedules = useAppStore((s) => s.schedules)
|
|
13
13
|
const loadSchedules = useAppStore((s) => s.loadSchedules)
|
|
14
14
|
const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
|
|
15
|
+
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
15
16
|
const [search, setSearch] = useState('')
|
|
16
17
|
|
|
17
18
|
useEffect(() => { loadSchedules() }, [])
|
|
@@ -20,10 +21,11 @@ export function ScheduleList({ inSidebar }: Props) {
|
|
|
20
21
|
return Object.values(schedules)
|
|
21
22
|
.filter((s) => {
|
|
22
23
|
if (search && !s.name.toLowerCase().includes(search.toLowerCase())) return false
|
|
24
|
+
if (activeProjectFilter && s.projectId !== activeProjectFilter) return false
|
|
23
25
|
return true
|
|
24
26
|
})
|
|
25
27
|
.sort((a, b) => b.createdAt - a.createdAt)
|
|
26
|
-
}, [schedules, search])
|
|
28
|
+
}, [schedules, search, activeProjectFilter])
|
|
27
29
|
|
|
28
30
|
if (!filtered.length && !search) {
|
|
29
31
|
return (
|
|
@@ -129,7 +129,7 @@ export function NewSessionSheet() {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
const handleCreate = async () => {
|
|
132
|
-
const sessionName = name.trim() || 'New
|
|
132
|
+
const sessionName = name.trim() || 'New Chat'
|
|
133
133
|
const cwd = selectedDir || ''
|
|
134
134
|
const resolvedCredentialId = currentProvider?.requiresApiKey
|
|
135
135
|
? credentialId
|
|
@@ -174,14 +174,14 @@ export function NewSessionSheet() {
|
|
|
174
174
|
<BottomSheet open={open} onClose={onClose} wide>
|
|
175
175
|
{/* Header */}
|
|
176
176
|
<div className="mb-10">
|
|
177
|
-
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">New
|
|
178
|
-
<p className="text-[14px] text-text-3">Configure your AI
|
|
177
|
+
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">New Chat</h2>
|
|
178
|
+
<p className="text-[14px] text-text-3">Configure your AI chat</p>
|
|
179
179
|
</div>
|
|
180
180
|
|
|
181
181
|
{/* Name */}
|
|
182
182
|
<div className="mb-8">
|
|
183
183
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
184
|
-
|
|
184
|
+
Chat Name
|
|
185
185
|
</label>
|
|
186
186
|
<input
|
|
187
187
|
type="text"
|
|
@@ -373,7 +373,7 @@ export function NewSessionSheet() {
|
|
|
373
373
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
374
374
|
Tools <span className="normal-case tracking-normal font-normal text-text-3">(optional)</span>
|
|
375
375
|
</label>
|
|
376
|
-
<p className="text-[12px] text-text-3/60 mb-3">Allow this model to execute commands and access files in the
|
|
376
|
+
<p className="text-[12px] text-text-3/60 mb-3">Allow this model to execute commands and access files in the working directory.</p>
|
|
377
377
|
<div className="flex flex-wrap gap-2.5">
|
|
378
378
|
{([
|
|
379
379
|
{ id: 'shell' as SessionTool, label: 'Shell' },
|
|
@@ -469,7 +469,7 @@ export function NewSessionSheet() {
|
|
|
469
469
|
shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
|
|
470
470
|
style={{ fontFamily: 'inherit' }}
|
|
471
471
|
>
|
|
472
|
-
Create
|
|
472
|
+
Create Chat
|
|
473
473
|
</button>
|
|
474
474
|
</div>
|
|
475
475
|
</BottomSheet>
|
|
@@ -117,7 +117,7 @@ export function SessionCard({ session, active, onClick }: Props) {
|
|
|
117
117
|
onClick={handleDelete}
|
|
118
118
|
className="shrink-0 opacity-0 group-hover/card:opacity-100 transition-opacity duration-150
|
|
119
119
|
text-text-3 hover:text-red-400 p-0.5 -mr-1 cursor-pointer bg-transparent border-none"
|
|
120
|
-
title="Delete
|
|
120
|
+
title="Delete chat"
|
|
121
121
|
>
|
|
122
122
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
123
123
|
<line x1="18" y1="6" x2="6" y2="18" /><line x1="6" y1="6" x2="18" y2="18" />
|
|
@@ -89,7 +89,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
89
89
|
<path d="M12 2L14.5 9.5L22 12L14.5 14.5L12 22L9.5 14.5L2 12L9.5 9.5L12 2Z" fill="currentColor" />
|
|
90
90
|
</svg>
|
|
91
91
|
</div>
|
|
92
|
-
<p className="font-display text-[15px] font-600 text-text-2">No
|
|
92
|
+
<p className="font-display text-[15px] font-600 text-text-2">No chats yet</p>
|
|
93
93
|
<p className="text-[13px] text-text-3/50">Create one to start chatting</p>
|
|
94
94
|
{!inSidebar && (
|
|
95
95
|
<button
|
|
@@ -99,7 +99,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
99
99
|
shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
|
|
100
100
|
style={{ fontFamily: 'inherit' }}
|
|
101
101
|
>
|
|
102
|
-
+ New
|
|
102
|
+
+ New Chat
|
|
103
103
|
</button>
|
|
104
104
|
)}
|
|
105
105
|
</div>
|
|
@@ -124,12 +124,12 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
124
124
|
{filtered.length > 0 && (
|
|
125
125
|
<button
|
|
126
126
|
onClick={async () => {
|
|
127
|
-
if (!window.confirm(`Delete ${filtered.length}
|
|
127
|
+
if (!window.confirm(`Delete ${filtered.length} chat${filtered.length === 1 ? '' : 's'}?`)) return
|
|
128
128
|
await clearSessions(filtered.map((s) => s.id))
|
|
129
129
|
}}
|
|
130
130
|
className="ml-auto p-1.5 rounded-[8px] text-text-3/70 hover:text-red-400 hover:bg-red-400/[0.06]
|
|
131
131
|
cursor-pointer transition-all bg-transparent border-none"
|
|
132
|
-
title="Clear all
|
|
132
|
+
title="Clear all chats"
|
|
133
133
|
>
|
|
134
134
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
135
135
|
<polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
|
|
@@ -154,7 +154,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
154
154
|
<select
|
|
155
155
|
value={sortMode}
|
|
156
156
|
onChange={(e) => setSortMode(e.target.value as SortMode)}
|
|
157
|
-
aria-label="Sort
|
|
157
|
+
aria-label="Sort chats"
|
|
158
158
|
className="px-2 py-2 rounded-[12px] border border-white/[0.04] bg-surface text-text
|
|
159
159
|
text-[11px] outline-none cursor-pointer"
|
|
160
160
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -176,7 +176,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
176
176
|
/>
|
|
177
177
|
<button
|
|
178
178
|
onClick={(e) => { e.stopPropagation(); togglePinSession(s.id) }}
|
|
179
|
-
aria-label={s.pinned ? 'Unpin
|
|
179
|
+
aria-label={s.pinned ? 'Unpin chat' : 'Pin chat'}
|
|
180
180
|
className={`absolute top-2 right-2 p-1 rounded-[6px] border-none cursor-pointer transition-all
|
|
181
181
|
${s.pinned
|
|
182
182
|
? 'text-amber-400 bg-amber-400/10 opacity-100'
|
|
@@ -193,7 +193,7 @@ export function SessionList({ inSidebar, onSelect }: Props) {
|
|
|
193
193
|
) : (
|
|
194
194
|
<div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
|
|
195
195
|
<p className="text-[13px] text-text-3/50">
|
|
196
|
-
No {typeFilter === 'orchestrated' ? 'AI' : typeFilter === 'active' ? 'active' : typeFilter}
|
|
196
|
+
No {typeFilter === 'orchestrated' ? 'AI' : typeFilter === 'active' ? 'active' : typeFilter} chats{search ? ` matching "${search}"` : ''}
|
|
197
197
|
</p>
|
|
198
198
|
</div>
|
|
199
199
|
)}
|
|
@@ -2,6 +2,7 @@ import type { Connector, ConnectorPlatform, Session } from '@/types'
|
|
|
2
2
|
import { cn } from '@/lib/utils'
|
|
3
3
|
import { BsMicrosoftTeams } from 'react-icons/bs'
|
|
4
4
|
import {
|
|
5
|
+
SiApple,
|
|
5
6
|
SiDiscord,
|
|
6
7
|
SiGooglechat,
|
|
7
8
|
SiMatrix,
|
|
@@ -17,6 +18,7 @@ export const CONNECTOR_PLATFORM_META: Record<ConnectorPlatform, { label: string;
|
|
|
17
18
|
slack: { label: 'Slack', color: '#4A154B' },
|
|
18
19
|
whatsapp: { label: 'WhatsApp', color: '#25D366' },
|
|
19
20
|
openclaw: { label: 'OpenClaw', color: '#F97316' },
|
|
21
|
+
bluebubbles: { label: 'BlueBubbles', color: '#2E89FF' },
|
|
20
22
|
signal: { label: 'Signal', color: '#3A76F0' },
|
|
21
23
|
teams: { label: 'Teams', color: '#6264A7' },
|
|
22
24
|
googlechat: { label: 'Google Chat', color: '#00AC47' },
|
|
@@ -62,6 +64,8 @@ export function ConnectorPlatformIcon({
|
|
|
62
64
|
return <SiSlack size={size} className={className} />
|
|
63
65
|
case 'whatsapp':
|
|
64
66
|
return <SiWhatsapp size={size} className={className} />
|
|
67
|
+
case 'bluebubbles':
|
|
68
|
+
return <SiApple size={size} className={className} />
|
|
65
69
|
case 'signal':
|
|
66
70
|
return <SiSignal size={size} className={className} />
|
|
67
71
|
case 'googlechat':
|
|
@@ -26,7 +26,7 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
|
|
|
26
26
|
`Stopped heartbeat on ${result.updatedSessions} session(s); cancelled ${result.cancelledQueued} queued run(s), aborted ${result.abortedRunning} running run(s).`,
|
|
27
27
|
)
|
|
28
28
|
} catch (err: any) {
|
|
29
|
-
setHeartbeatBulkNotice(err?.message || 'Failed to disable heartbeat for all
|
|
29
|
+
setHeartbeatBulkNotice(err?.message || 'Failed to disable heartbeat for all agents.')
|
|
30
30
|
} finally {
|
|
31
31
|
setDisablingHeartbeats(false)
|
|
32
32
|
}
|
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import { useAppStore } from '@/stores/use-app-store'
|
|
4
4
|
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
5
|
+
import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
|
|
5
6
|
import type { SettingsSectionProps } from './types'
|
|
6
7
|
|
|
7
|
-
const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
|
|
8
|
-
|
|
9
8
|
export function OrchestratorSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
|
|
10
9
|
const providers = useAppStore((s) => s.providers)
|
|
11
10
|
const credentials = useAppStore((s) => s.credentials)
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { SettingsSectionProps } from './types'
|
|
4
|
+
|
|
5
|
+
export function WebSearchSection({ appSettings, patchSettings, inputClass }: SettingsSectionProps) {
|
|
6
|
+
const provider = appSettings.webSearchProvider || 'duckduckgo'
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="mb-10">
|
|
10
|
+
<h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
11
|
+
Web Search
|
|
12
|
+
</h3>
|
|
13
|
+
<p className="text-[12px] text-text-3 mb-5">
|
|
14
|
+
Choose which search engine agents use for the <code className="text-[11px] font-mono text-text-2">web_search</code> tool.
|
|
15
|
+
</p>
|
|
16
|
+
<div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
|
|
17
|
+
<div className="mb-5">
|
|
18
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Search Provider</label>
|
|
19
|
+
<select
|
|
20
|
+
value={provider}
|
|
21
|
+
onChange={(e) => patchSettings({ webSearchProvider: e.target.value as typeof provider })}
|
|
22
|
+
className={inputClass}
|
|
23
|
+
style={{ fontFamily: 'inherit' }}
|
|
24
|
+
>
|
|
25
|
+
<option value="duckduckgo">DuckDuckGo (default, no key required)</option>
|
|
26
|
+
<option value="google">Google (scraping, no key required)</option>
|
|
27
|
+
<option value="bing">Bing (scraping, no key required)</option>
|
|
28
|
+
<option value="searxng">SearXNG (self-hosted, no key required)</option>
|
|
29
|
+
<option value="tavily">Tavily (requires API key in Secrets)</option>
|
|
30
|
+
<option value="brave">Brave Search (requires API key in Secrets)</option>
|
|
31
|
+
</select>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{provider === 'searxng' && (
|
|
35
|
+
<div>
|
|
36
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">SearXNG URL</label>
|
|
37
|
+
<input
|
|
38
|
+
type="text"
|
|
39
|
+
value={appSettings.searxngUrl || ''}
|
|
40
|
+
onChange={(e) => patchSettings({ searxngUrl: e.target.value || undefined })}
|
|
41
|
+
placeholder="http://localhost:8080"
|
|
42
|
+
className={inputClass}
|
|
43
|
+
style={{ fontFamily: 'inherit' }}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
|
|
48
|
+
{(provider === 'tavily' || provider === 'brave') && (
|
|
49
|
+
<p className="text-[11px] text-text-3/70">
|
|
50
|
+
Add a secret named "{provider}" or "{provider}_api_key" in the Secrets section below.
|
|
51
|
+
</p>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react'
|
|
4
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { inputClass } from './utils'
|
|
6
|
+
import { UserPreferencesSection } from './section-user-preferences'
|
|
7
|
+
import { OrchestratorSection } from './section-orchestrator'
|
|
8
|
+
import { RuntimeLoopSection } from './section-runtime-loop'
|
|
9
|
+
import { CapabilityPolicySection } from './section-capability-policy'
|
|
10
|
+
import { VoiceSection } from './section-voice'
|
|
11
|
+
import { WebSearchSection } from './section-web-search'
|
|
12
|
+
import { HeartbeatSection } from './section-heartbeat'
|
|
13
|
+
import { EmbeddingSection } from './section-embedding'
|
|
14
|
+
import { MemorySection } from './section-memory'
|
|
15
|
+
import { SecretsSection } from './section-secrets'
|
|
16
|
+
import { ProvidersSection } from './section-providers'
|
|
17
|
+
import { PluginManager } from './plugin-manager'
|
|
18
|
+
|
|
19
|
+
export function SettingsPage() {
|
|
20
|
+
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
21
|
+
const loadCredentials = useAppStore((s) => s.loadCredentials)
|
|
22
|
+
const appSettings = useAppStore((s) => s.appSettings)
|
|
23
|
+
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
24
|
+
const updateSettings = useAppStore((s) => s.updateSettings)
|
|
25
|
+
const loadSecrets = useAppStore((s) => s.loadSecrets)
|
|
26
|
+
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
27
|
+
const credentials = useAppStore((s) => s.credentials)
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
loadProviders()
|
|
31
|
+
loadCredentials()
|
|
32
|
+
loadSettings()
|
|
33
|
+
loadSecrets()
|
|
34
|
+
loadAgents()
|
|
35
|
+
}, [])
|
|
36
|
+
|
|
37
|
+
const credList = Object.values(credentials)
|
|
38
|
+
const patchSettings = updateSettings
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex-1 flex flex-col h-full overflow-y-auto">
|
|
42
|
+
<div className="w-full max-w-3xl mx-auto px-6 py-8">
|
|
43
|
+
<div className="mb-10">
|
|
44
|
+
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">Settings</h2>
|
|
45
|
+
<p className="text-[14px] text-text-3">Manage providers, API keys & orchestrator engine</p>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<UserPreferencesSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
|
|
49
|
+
<OrchestratorSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
|
|
50
|
+
<RuntimeLoopSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
|
|
51
|
+
<CapabilityPolicySection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
|
|
52
|
+
<WebSearchSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
|
|
53
|
+
<VoiceSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
|
|
54
|
+
<HeartbeatSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
|
|
55
|
+
<EmbeddingSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} credList={credList} />
|
|
56
|
+
<MemorySection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
|
|
57
|
+
<SecretsSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
|
|
58
|
+
<ProvidersSection appSettings={appSettings} patchSettings={patchSettings} inputClass={inputClass} />
|
|
59
|
+
|
|
60
|
+
<div className="mb-10">
|
|
61
|
+
<h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
62
|
+
Plugins
|
|
63
|
+
</h3>
|
|
64
|
+
<p className="text-[12px] text-text-3 mb-5">
|
|
65
|
+
Extend agent behavior with hooks. Install from the marketplace, a URL, or drop .js files into <code className="text-[11px] font-mono text-text-2">data/plugins/</code>.
|
|
66
|
+
<span className="text-text-3/70 ml-1">OpenClaw plugins are also supported.</span>
|
|
67
|
+
</p>
|
|
68
|
+
<PluginManager />
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
@@ -29,6 +29,7 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
29
29
|
const loadSkills = useAppStore((s) => s.loadSkills)
|
|
30
30
|
const setSkillSheetOpen = useAppStore((s) => s.setSkillSheetOpen)
|
|
31
31
|
const setEditingSkillId = useAppStore((s) => s.setEditingSkillId)
|
|
32
|
+
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
32
33
|
const [clawHubOpen, setClawHubOpen] = useState(false)
|
|
33
34
|
|
|
34
35
|
// Embedded ClawHub state (full-width only)
|
|
@@ -46,7 +47,7 @@ export function SkillList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
46
47
|
loadSkills()
|
|
47
48
|
}, [])
|
|
48
49
|
|
|
49
|
-
const skillList = Object.values(skills)
|
|
50
|
+
const skillList = Object.values(skills).filter((s) => !activeProjectFilter || s.projectId === activeProjectFilter)
|
|
50
51
|
|
|
51
52
|
const handleEdit = (id: string) => {
|
|
52
53
|
setEditingSkillId(id)
|
|
@@ -21,6 +21,7 @@ export function TaskList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
21
21
|
const agents = useAppStore((s) => s.agents)
|
|
22
22
|
const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
|
|
23
23
|
const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
|
|
24
|
+
const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
|
|
24
25
|
const [search, setSearch] = useState('')
|
|
25
26
|
const [clearing, setClearing] = useState(false)
|
|
26
27
|
|
|
@@ -28,8 +29,10 @@ export function TaskList({ inSidebar }: { inSidebar?: boolean }) {
|
|
|
28
29
|
useWs('tasks', loadTasks, 5000)
|
|
29
30
|
|
|
30
31
|
const sorted = useMemo(() =>
|
|
31
|
-
Object.values(tasks)
|
|
32
|
-
|
|
32
|
+
Object.values(tasks)
|
|
33
|
+
.filter((t) => !activeProjectFilter || t.projectId === activeProjectFilter)
|
|
34
|
+
.sort((a, b) => b.updatedAt - a.updatedAt),
|
|
35
|
+
[tasks, activeProjectFilter],
|
|
33
36
|
)
|
|
34
37
|
|
|
35
38
|
const filtered = useMemo(() => {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useRef, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
export type ContinuousSpeechState = 'idle' | 'listening' | 'cooldown' | 'waitingForResponse'
|
|
6
|
+
|
|
7
|
+
interface UseContinuousSpeechOptions {
|
|
8
|
+
lang?: string
|
|
9
|
+
silenceDelayMs?: number
|
|
10
|
+
onUtterance: (transcript: string) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
14
|
+
const { lang, silenceDelayMs = 800, onUtterance } = options
|
|
15
|
+
const [state, setState] = useState<ContinuousSpeechState>('idle')
|
|
16
|
+
const [transcript, setTranscript] = useState('')
|
|
17
|
+
const [interimText, setInterimText] = useState('')
|
|
18
|
+
|
|
19
|
+
const recogRef = useRef<any>(null)
|
|
20
|
+
const silenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
21
|
+
const activeRef = useRef(false)
|
|
22
|
+
const accumulatedRef = useRef('')
|
|
23
|
+
|
|
24
|
+
const clearSilenceTimer = () => {
|
|
25
|
+
if (silenceTimerRef.current) {
|
|
26
|
+
clearTimeout(silenceTimerRef.current)
|
|
27
|
+
silenceTimerRef.current = null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const startRecognition = useCallback(() => {
|
|
32
|
+
const SR = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
|
|
33
|
+
if (!SR) return
|
|
34
|
+
|
|
35
|
+
if (recogRef.current) {
|
|
36
|
+
try { recogRef.current.stop() } catch { /* noop */ }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const recog = new SR()
|
|
40
|
+
recog.continuous = true
|
|
41
|
+
recog.interimResults = true
|
|
42
|
+
recog.maxAlternatives = 1
|
|
43
|
+
recog.lang = lang || navigator.language || 'en-US'
|
|
44
|
+
|
|
45
|
+
recog.onresult = (e: any) => {
|
|
46
|
+
clearSilenceTimer()
|
|
47
|
+
let interim = ''
|
|
48
|
+
let final = ''
|
|
49
|
+
|
|
50
|
+
for (let i = e.resultIndex; i < e.results.length; i++) {
|
|
51
|
+
const result = e.results[i]
|
|
52
|
+
if (result.isFinal) {
|
|
53
|
+
final += result[0].transcript
|
|
54
|
+
} else {
|
|
55
|
+
interim += result[0].transcript
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (final) {
|
|
60
|
+
accumulatedRef.current += (accumulatedRef.current ? ' ' : '') + final.trim()
|
|
61
|
+
setTranscript(accumulatedRef.current)
|
|
62
|
+
setInterimText('')
|
|
63
|
+
|
|
64
|
+
// Start silence timer — after delay, send the utterance
|
|
65
|
+
silenceTimerRef.current = setTimeout(() => {
|
|
66
|
+
if (!activeRef.current) return
|
|
67
|
+
const text = accumulatedRef.current.trim()
|
|
68
|
+
if (text) {
|
|
69
|
+
setState('waitingForResponse')
|
|
70
|
+
onUtterance(text)
|
|
71
|
+
accumulatedRef.current = ''
|
|
72
|
+
setTranscript('')
|
|
73
|
+
}
|
|
74
|
+
}, silenceDelayMs)
|
|
75
|
+
} else {
|
|
76
|
+
setInterimText(interim)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
recog.onerror = (e: any) => {
|
|
81
|
+
// 'no-speech' is normal during silence; 'aborted' when stopping intentionally
|
|
82
|
+
if (e.error === 'no-speech' || e.error === 'aborted') return
|
|
83
|
+
console.warn('[continuous-speech] error:', e.error)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
recog.onend = () => {
|
|
87
|
+
// Auto-restart if still active (browser may stop recognition periodically)
|
|
88
|
+
if (activeRef.current && state !== 'waitingForResponse') {
|
|
89
|
+
try { recog.start() } catch { /* noop */ }
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
recogRef.current = recog
|
|
94
|
+
try {
|
|
95
|
+
recog.start()
|
|
96
|
+
setState('listening')
|
|
97
|
+
} catch {
|
|
98
|
+
setState('idle')
|
|
99
|
+
}
|
|
100
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
101
|
+
}, [lang, silenceDelayMs, onUtterance])
|
|
102
|
+
|
|
103
|
+
const start = useCallback(() => {
|
|
104
|
+
activeRef.current = true
|
|
105
|
+
accumulatedRef.current = ''
|
|
106
|
+
setTranscript('')
|
|
107
|
+
setInterimText('')
|
|
108
|
+
startRecognition()
|
|
109
|
+
}, [startRecognition])
|
|
110
|
+
|
|
111
|
+
const stop = useCallback(() => {
|
|
112
|
+
activeRef.current = false
|
|
113
|
+
clearSilenceTimer()
|
|
114
|
+
if (recogRef.current) {
|
|
115
|
+
try { recogRef.current.stop() } catch { /* noop */ }
|
|
116
|
+
recogRef.current = null
|
|
117
|
+
}
|
|
118
|
+
setState('idle')
|
|
119
|
+
setTranscript('')
|
|
120
|
+
setInterimText('')
|
|
121
|
+
accumulatedRef.current = ''
|
|
122
|
+
}, [])
|
|
123
|
+
|
|
124
|
+
const pause = useCallback(() => {
|
|
125
|
+
clearSilenceTimer()
|
|
126
|
+
if (recogRef.current) {
|
|
127
|
+
try { recogRef.current.stop() } catch { /* noop */ }
|
|
128
|
+
}
|
|
129
|
+
}, [])
|
|
130
|
+
|
|
131
|
+
const resume = useCallback(() => {
|
|
132
|
+
if (!activeRef.current) return
|
|
133
|
+
accumulatedRef.current = ''
|
|
134
|
+
setTranscript('')
|
|
135
|
+
setInterimText('')
|
|
136
|
+
setState('listening')
|
|
137
|
+
startRecognition()
|
|
138
|
+
}, [startRecognition])
|
|
139
|
+
|
|
140
|
+
const supported = typeof window !== 'undefined' &&
|
|
141
|
+
!!((window as any).SpeechRecognition || (window as any).webkitSpeechRecognition)
|
|
142
|
+
|
|
143
|
+
return { state, transcript, interimText, start, stop, pause, resume, supported }
|
|
144
|
+
}
|