@swarmclawai/swarmclaw 0.4.5 → 0.5.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 +8 -2
- package/bin/server-cmd.js +28 -19
- package/next.config.ts +5 -0
- package/package.json +2 -1
- package/src/app/api/agents/[id]/route.ts +23 -5
- package/src/app/api/agents/trash/route.ts +44 -0
- package/src/app/api/connectors/[id]/route.ts +7 -4
- package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
- package/src/app/api/openclaw/agent-files/route.ts +57 -0
- package/src/app/api/openclaw/approvals/route.ts +46 -0
- package/src/app/api/openclaw/config-sync/route.ts +33 -0
- package/src/app/api/openclaw/cron/route.ts +52 -0
- package/src/app/api/openclaw/directory/route.ts +4 -3
- package/src/app/api/openclaw/discover/route.ts +3 -2
- package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
- package/src/app/api/openclaw/exec-config/route.ts +41 -0
- package/src/app/api/openclaw/gateway/route.ts +72 -0
- package/src/app/api/openclaw/history/route.ts +109 -0
- package/src/app/api/openclaw/media/route.ts +53 -0
- package/src/app/api/openclaw/models/route.ts +12 -0
- package/src/app/api/openclaw/permissions/route.ts +39 -0
- package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
- package/src/app/api/openclaw/skills/install/route.ts +32 -0
- package/src/app/api/openclaw/skills/remove/route.ts +24 -0
- package/src/app/api/openclaw/skills/route.ts +82 -0
- package/src/app/api/openclaw/sync/route.ts +3 -2
- package/src/app/api/projects/[id]/route.ts +1 -1
- package/src/app/api/projects/route.ts +1 -1
- package/src/app/api/secrets/[id]/route.ts +1 -1
- package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
- package/src/app/api/sessions/[id]/fork/route.ts +44 -0
- package/src/app/api/sessions/[id]/messages/route.ts +18 -1
- package/src/app/api/sessions/[id]/route.ts +12 -3
- package/src/app/api/sessions/route.ts +6 -2
- package/src/app/globals.css +14 -0
- package/src/app/layout.tsx +5 -20
- package/src/cli/index.js +33 -1
- package/src/cli/spec.js +40 -0
- package/src/components/agents/agent-avatar.tsx +45 -0
- package/src/components/agents/agent-card.tsx +19 -5
- package/src/components/agents/agent-chat-list.tsx +31 -24
- package/src/components/agents/agent-files-editor.tsx +185 -0
- package/src/components/agents/agent-list.tsx +82 -3
- package/src/components/agents/agent-sheet.tsx +31 -0
- package/src/components/agents/cron-job-form.tsx +137 -0
- package/src/components/agents/exec-config-panel.tsx +147 -0
- package/src/components/agents/inspector-panel.tsx +310 -0
- package/src/components/agents/openclaw-skills-panel.tsx +230 -0
- package/src/components/agents/permission-preset-selector.tsx +79 -0
- package/src/components/agents/personality-builder.tsx +111 -0
- package/src/components/agents/sandbox-env-panel.tsx +72 -0
- package/src/components/agents/skill-install-dialog.tsx +102 -0
- package/src/components/agents/trash-list.tsx +109 -0
- package/src/components/chat/chat-area.tsx +14 -2
- package/src/components/chat/chat-header.tsx +168 -4
- package/src/components/chat/chat-preview-panel.tsx +113 -0
- package/src/components/chat/exec-approval-card.tsx +89 -0
- package/src/components/chat/message-bubble.tsx +218 -36
- package/src/components/chat/message-list.tsx +135 -31
- package/src/components/chat/streaming-bubble.tsx +59 -10
- package/src/components/chat/suggestions-bar.tsx +74 -0
- package/src/components/chat/thinking-indicator.tsx +20 -6
- package/src/components/chat/tool-call-bubble.tsx +89 -16
- package/src/components/chat/tool-request-banner.tsx +20 -2
- package/src/components/chat/trace-block.tsx +103 -0
- package/src/components/projects/project-list.tsx +1 -0
- package/src/components/settings/gateway-connection-panel.tsx +278 -0
- package/src/components/shared/avatar.tsx +13 -2
- package/src/components/shared/settings/settings-page.tsx +1 -0
- package/src/components/tasks/task-board.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +12 -12
- package/src/hooks/use-continuous-speech.ts +42 -5
- package/src/hooks/use-openclaw-gateway.ts +63 -0
- package/src/lib/notification-sounds.ts +58 -0
- package/src/lib/personality-parser.ts +97 -0
- package/src/lib/providers/openclaw.ts +17 -2
- package/src/lib/runtime-loop.ts +2 -2
- package/src/lib/server/chat-execution.ts +44 -2
- package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
- package/src/lib/server/connectors/bluebubbles.ts +5 -2
- package/src/lib/server/connectors/googlechat.ts +14 -10
- package/src/lib/server/connectors/manager.ts +37 -15
- package/src/lib/server/connectors/openclaw.ts +1 -0
- package/src/lib/server/daemon-state.ts +11 -0
- package/src/lib/server/main-agent-loop.ts +2 -3
- package/src/lib/server/main-session.ts +21 -0
- package/src/lib/server/openclaw-config-sync.ts +107 -0
- package/src/lib/server/openclaw-exec-config.ts +52 -0
- package/src/lib/server/openclaw-gateway.ts +291 -0
- package/src/lib/server/openclaw-history-merge.ts +36 -0
- package/src/lib/server/openclaw-models.ts +56 -0
- package/src/lib/server/openclaw-permission-presets.ts +64 -0
- package/src/lib/server/openclaw-sync.ts +11 -10
- package/src/lib/server/queue.ts +2 -1
- package/src/lib/server/session-tools/connector.ts +4 -4
- package/src/lib/server/session-tools/delegate.ts +19 -3
- package/src/lib/server/session-tools/file.ts +20 -20
- package/src/lib/server/session-tools/index.ts +2 -2
- package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
- package/src/lib/server/session-tools/sandbox.ts +2 -2
- package/src/lib/server/session-tools/search-providers.ts +13 -6
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/web.ts +8 -8
- package/src/lib/server/storage.ts +62 -11
- package/src/lib/server/stream-agent-chat.ts +24 -4
- package/src/lib/server/suggestions.ts +20 -0
- package/src/lib/server/ws-hub.ts +14 -0
- package/src/stores/use-app-store.ts +53 -1
- package/src/stores/use-approval-store.ts +78 -0
- package/src/stores/use-chat-store.ts +153 -5
- package/src/types/index.ts +127 -1
- package/tsconfig.json +13 -4
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import type { GatewayReloadMode } from '@/types'
|
|
6
|
+
|
|
7
|
+
type ConnectionStatus = 'disconnected' | 'connecting' | 'connected'
|
|
8
|
+
|
|
9
|
+
interface ConfigIssue {
|
|
10
|
+
id: string
|
|
11
|
+
severity: 'warning' | 'error'
|
|
12
|
+
title: string
|
|
13
|
+
description: string
|
|
14
|
+
repairAction?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function GatewayConnectionPanel() {
|
|
18
|
+
const [url, setUrl] = useState('')
|
|
19
|
+
const [token, setToken] = useState('')
|
|
20
|
+
const [status, setStatus] = useState<ConnectionStatus>('disconnected')
|
|
21
|
+
const [error, setError] = useState('')
|
|
22
|
+
const [reloadMode, setReloadMode] = useState<GatewayReloadMode>('hot')
|
|
23
|
+
const [reloadSaving, setReloadSaving] = useState(false)
|
|
24
|
+
const [issues, setIssues] = useState<ConfigIssue[]>([])
|
|
25
|
+
const [repairingId, setRepairingId] = useState<string | null>(null)
|
|
26
|
+
|
|
27
|
+
const checkStatus = useCallback(async () => {
|
|
28
|
+
try {
|
|
29
|
+
const res = await api<{ connected: boolean }>('GET', '/openclaw/gateway')
|
|
30
|
+
setStatus(res.connected ? 'connected' : 'disconnected')
|
|
31
|
+
} catch {
|
|
32
|
+
setStatus('disconnected')
|
|
33
|
+
}
|
|
34
|
+
}, [])
|
|
35
|
+
|
|
36
|
+
const loadReloadMode = useCallback(async () => {
|
|
37
|
+
try {
|
|
38
|
+
const res = await api<{ ok: boolean; result: GatewayReloadMode }>('POST', '/openclaw/gateway', {
|
|
39
|
+
method: 'gateway.reload-mode.get',
|
|
40
|
+
params: {},
|
|
41
|
+
})
|
|
42
|
+
if (res.ok && res.result) setReloadMode(res.result)
|
|
43
|
+
} catch {
|
|
44
|
+
// ignore — gateway may not be connected
|
|
45
|
+
}
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
const loadIssues = useCallback(async () => {
|
|
49
|
+
try {
|
|
50
|
+
const res = await api<{ issues: ConfigIssue[] }>('GET', '/openclaw/config-sync')
|
|
51
|
+
setIssues(res.issues ?? [])
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
checkStatus()
|
|
59
|
+
}, [checkStatus])
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (status === 'connected') {
|
|
63
|
+
loadReloadMode()
|
|
64
|
+
loadIssues()
|
|
65
|
+
}
|
|
66
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
|
+
}, [status])
|
|
68
|
+
|
|
69
|
+
const handleConnect = async () => {
|
|
70
|
+
setStatus('connecting')
|
|
71
|
+
setError('')
|
|
72
|
+
try {
|
|
73
|
+
const res = await api<{ ok: boolean; error?: string }>('POST', '/openclaw/gateway', {
|
|
74
|
+
method: 'gateway.connect',
|
|
75
|
+
params: { url: url || undefined, token: token || undefined },
|
|
76
|
+
})
|
|
77
|
+
if (res.ok) {
|
|
78
|
+
setStatus('connected')
|
|
79
|
+
} else {
|
|
80
|
+
setStatus('disconnected')
|
|
81
|
+
setError(res.error || 'Connection failed')
|
|
82
|
+
}
|
|
83
|
+
} catch (err: unknown) {
|
|
84
|
+
setStatus('disconnected')
|
|
85
|
+
setError(err instanceof Error ? err.message : 'Connection failed')
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const handleDisconnect = async () => {
|
|
90
|
+
setError('')
|
|
91
|
+
try {
|
|
92
|
+
await api('POST', '/openclaw/gateway', {
|
|
93
|
+
method: 'gateway.disconnect',
|
|
94
|
+
params: {},
|
|
95
|
+
})
|
|
96
|
+
setStatus('disconnected')
|
|
97
|
+
setIssues([])
|
|
98
|
+
} catch (err: unknown) {
|
|
99
|
+
setError(err instanceof Error ? err.message : 'Disconnect failed')
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const handleReloadModeChange = async (mode: GatewayReloadMode) => {
|
|
104
|
+
setReloadSaving(true)
|
|
105
|
+
try {
|
|
106
|
+
await api('POST', '/openclaw/gateway', {
|
|
107
|
+
method: 'gateway.reload-mode.set',
|
|
108
|
+
params: { mode },
|
|
109
|
+
})
|
|
110
|
+
setReloadMode(mode)
|
|
111
|
+
} catch {
|
|
112
|
+
// ignore
|
|
113
|
+
} finally {
|
|
114
|
+
setReloadSaving(false)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const handleRepair = async (issueId: string) => {
|
|
119
|
+
setRepairingId(issueId)
|
|
120
|
+
try {
|
|
121
|
+
await api('POST', '/openclaw/config-sync', { issueId })
|
|
122
|
+
await loadIssues()
|
|
123
|
+
} catch {
|
|
124
|
+
// ignore
|
|
125
|
+
} finally {
|
|
126
|
+
setRepairingId(null)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const dotColor = status === 'connected'
|
|
131
|
+
? 'bg-emerald-400'
|
|
132
|
+
: status === 'connecting'
|
|
133
|
+
? 'bg-amber-400 animate-pulse'
|
|
134
|
+
: 'bg-red-400'
|
|
135
|
+
|
|
136
|
+
const reloadModes: { value: GatewayReloadMode; label: string; desc: string }[] = [
|
|
137
|
+
{ value: 'hot', label: 'Hot', desc: 'Only reload changed agents' },
|
|
138
|
+
{ value: 'hybrid', label: 'Hybrid', desc: 'Hot + restart stale sessions' },
|
|
139
|
+
{ value: 'full', label: 'Full', desc: 'Restart all agents on change' },
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
const actionableIssues = issues.filter((i) => i.id !== 'no-connection')
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<div className="flex flex-col gap-4 p-4">
|
|
146
|
+
<div className="flex items-center gap-2">
|
|
147
|
+
<span className={`w-2 h-2 rounded-full ${dotColor}`} />
|
|
148
|
+
<span className="text-[13px] font-600 text-text capitalize">{status}</span>
|
|
149
|
+
{actionableIssues.length > 0 && (
|
|
150
|
+
<span className="ml-auto px-2 py-0.5 rounded-[6px] bg-amber-400/10 text-amber-400 text-[10px] font-700">
|
|
151
|
+
{actionableIssues.length} issue{actionableIssues.length !== 1 ? 's' : ''}
|
|
152
|
+
</span>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
<div className="flex flex-col gap-2">
|
|
157
|
+
<label className="text-[11px] font-600 uppercase tracking-wider text-text-3/50">Gateway URL</label>
|
|
158
|
+
<input
|
|
159
|
+
type="text"
|
|
160
|
+
value={url}
|
|
161
|
+
onChange={(e) => setUrl(e.target.value)}
|
|
162
|
+
placeholder="ws://127.0.0.1:18789"
|
|
163
|
+
className="w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text font-mono outline-none placeholder:text-text-3/40 focus:border-white/[0.12] transition-colors"
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div className="flex flex-col gap-2">
|
|
168
|
+
<label className="text-[11px] font-600 uppercase tracking-wider text-text-3/50">Token (optional)</label>
|
|
169
|
+
<input
|
|
170
|
+
type="password"
|
|
171
|
+
value={token}
|
|
172
|
+
onChange={(e) => setToken(e.target.value)}
|
|
173
|
+
placeholder="Access token"
|
|
174
|
+
className="w-full px-3 py-2 rounded-[10px] border border-white/[0.06] bg-black/20 text-[13px] text-text font-mono outline-none placeholder:text-text-3/40 focus:border-white/[0.12] transition-colors"
|
|
175
|
+
/>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div className="flex gap-2">
|
|
179
|
+
{status !== 'connected' ? (
|
|
180
|
+
<button
|
|
181
|
+
onClick={handleConnect}
|
|
182
|
+
disabled={status === 'connecting'}
|
|
183
|
+
className="px-4 py-2 rounded-[10px] border-none bg-accent-bright text-white text-[12px] font-600 cursor-pointer disabled:opacity-40 transition-all hover:brightness-110"
|
|
184
|
+
style={{ fontFamily: 'inherit' }}
|
|
185
|
+
>
|
|
186
|
+
{status === 'connecting' ? 'Connecting...' : 'Connect'}
|
|
187
|
+
</button>
|
|
188
|
+
) : (
|
|
189
|
+
<button
|
|
190
|
+
onClick={handleDisconnect}
|
|
191
|
+
className="px-4 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-red-400 text-[12px] font-600 cursor-pointer transition-all hover:bg-red-400/10"
|
|
192
|
+
style={{ fontFamily: 'inherit' }}
|
|
193
|
+
>
|
|
194
|
+
Disconnect
|
|
195
|
+
</button>
|
|
196
|
+
)}
|
|
197
|
+
<button
|
|
198
|
+
onClick={checkStatus}
|
|
199
|
+
className="px-4 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-3 text-[12px] font-600 cursor-pointer transition-all hover:bg-white/[0.04]"
|
|
200
|
+
style={{ fontFamily: 'inherit' }}
|
|
201
|
+
>
|
|
202
|
+
Refresh
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{error && (
|
|
207
|
+
<p className="text-[12px] text-red-400">{error}</p>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Reload Mode Toggle (F21) */}
|
|
211
|
+
{status === 'connected' && (
|
|
212
|
+
<div className="flex flex-col gap-2 pt-2 border-t border-white/[0.04]">
|
|
213
|
+
<label className="text-[11px] font-600 uppercase tracking-wider text-text-3/50">Reload Mode</label>
|
|
214
|
+
<div className="flex gap-1">
|
|
215
|
+
{reloadModes.map((rm) => (
|
|
216
|
+
<button
|
|
217
|
+
key={rm.value}
|
|
218
|
+
onClick={() => handleReloadModeChange(rm.value)}
|
|
219
|
+
disabled={reloadSaving}
|
|
220
|
+
className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border
|
|
221
|
+
${reloadMode === rm.value
|
|
222
|
+
? 'bg-accent-soft text-accent-bright border-accent-bright/30'
|
|
223
|
+
: 'bg-transparent text-text-3 border-white/[0.06] hover:border-white/[0.12]'
|
|
224
|
+
}`}
|
|
225
|
+
style={{ fontFamily: 'inherit' }}
|
|
226
|
+
title={rm.desc}
|
|
227
|
+
>
|
|
228
|
+
{rm.label}
|
|
229
|
+
</button>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
<p className="text-[10px] text-text-3/40">
|
|
233
|
+
{reloadModes.find((r) => r.value === reloadMode)?.desc}
|
|
234
|
+
</p>
|
|
235
|
+
</div>
|
|
236
|
+
)}
|
|
237
|
+
|
|
238
|
+
{/* Config Issues (F19) */}
|
|
239
|
+
{actionableIssues.length > 0 && (
|
|
240
|
+
<div className="flex flex-col gap-2 pt-2 border-t border-white/[0.04]">
|
|
241
|
+
<label className="text-[11px] font-600 uppercase tracking-wider text-text-3/50">Config Issues</label>
|
|
242
|
+
{actionableIssues.map((issue) => (
|
|
243
|
+
<div
|
|
244
|
+
key={issue.id}
|
|
245
|
+
className={`flex items-start gap-3 p-3 rounded-[10px] border ${
|
|
246
|
+
issue.severity === 'error'
|
|
247
|
+
? 'bg-red-400/[0.04] border-red-400/20'
|
|
248
|
+
: 'bg-amber-400/[0.04] border-amber-400/20'
|
|
249
|
+
}`}
|
|
250
|
+
>
|
|
251
|
+
<span className={`mt-0.5 w-2 h-2 rounded-full shrink-0 ${
|
|
252
|
+
issue.severity === 'error' ? 'bg-red-400' : 'bg-amber-400'
|
|
253
|
+
}`} />
|
|
254
|
+
<div className="flex-1 min-w-0">
|
|
255
|
+
<p className={`text-[12px] font-600 ${
|
|
256
|
+
issue.severity === 'error' ? 'text-red-400' : 'text-amber-400'
|
|
257
|
+
}`}>
|
|
258
|
+
{issue.title}
|
|
259
|
+
</p>
|
|
260
|
+
<p className="text-[11px] text-text-3/60 mt-0.5">{issue.description}</p>
|
|
261
|
+
</div>
|
|
262
|
+
{issue.repairAction && (
|
|
263
|
+
<button
|
|
264
|
+
onClick={() => handleRepair(issue.repairAction!)}
|
|
265
|
+
disabled={repairingId === issue.repairAction}
|
|
266
|
+
className="shrink-0 px-3 py-1 rounded-[7px] border-none bg-accent-bright text-white text-[10px] font-600 cursor-pointer disabled:opacity-40 transition-all hover:brightness-110"
|
|
267
|
+
style={{ fontFamily: 'inherit' }}
|
|
268
|
+
>
|
|
269
|
+
{repairingId === issue.repairAction ? 'Repairing...' : 'Repair'}
|
|
270
|
+
</button>
|
|
271
|
+
)}
|
|
272
|
+
</div>
|
|
273
|
+
))}
|
|
274
|
+
</div>
|
|
275
|
+
)}
|
|
276
|
+
</div>
|
|
277
|
+
)
|
|
278
|
+
}
|
|
@@ -34,11 +34,22 @@ export function Avatar({ user, size = 'md' }: Props) {
|
|
|
34
34
|
)
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export
|
|
37
|
+
export type AiMood = 'idle' | 'thinking' | 'tool' | 'error' | 'success'
|
|
38
|
+
|
|
39
|
+
const moodAnimClass: Record<AiMood, string> = {
|
|
40
|
+
idle: '',
|
|
41
|
+
thinking: 'ai-mood-pulse',
|
|
42
|
+
tool: 'ai-mood-glow',
|
|
43
|
+
error: 'ai-mood-shake',
|
|
44
|
+
success: 'ai-mood-bounce',
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function AiAvatar({ size = 'md', mood }: { size?: 'sm' | 'md'; mood?: AiMood }) {
|
|
38
48
|
const s = size === 'sm' ? 'w-6 h-6' : 'w-8 h-8'
|
|
39
49
|
const iconSize = size === 'sm' ? 12 : 16
|
|
50
|
+
const animCls = mood ? moodAnimClass[mood] : ''
|
|
40
51
|
return (
|
|
41
|
-
<div className={`${s} rounded-[8px] bg-accent-soft flex items-center justify-center shrink-0`}>
|
|
52
|
+
<div className={`${s} rounded-[8px] bg-accent-soft flex items-center justify-center shrink-0 ${animCls}`}>
|
|
42
53
|
<svg width={iconSize} height={iconSize} viewBox="0 0 24 24" fill="none" className="text-accent-bright">
|
|
43
54
|
<path d="M12 2L14.5 9.5L22 12L14.5 14.5L12 22L9.5 14.5L2 12L9.5 9.5L12 2Z"
|
|
44
55
|
fill="currentColor" />
|
|
@@ -44,7 +44,7 @@ export function TaskBoard() {
|
|
|
44
44
|
<div className="flex items-center justify-between px-8 pt-6 pb-4 shrink-0">
|
|
45
45
|
<div>
|
|
46
46
|
<h1 className="font-display text-[28px] font-800 tracking-[-0.03em]">Task Board</h1>
|
|
47
|
-
<p className="text-[13px] text-text-3 mt-1">Create tasks and assign
|
|
47
|
+
<p className="text-[13px] text-text-3 mt-1">Create tasks and assign agents to run them</p>
|
|
48
48
|
</div>
|
|
49
49
|
<div className="flex items-center gap-3">
|
|
50
50
|
<select
|
|
@@ -35,7 +35,7 @@ export function TaskSheet() {
|
|
|
35
35
|
const [file, setFile] = useState<string | null>(null)
|
|
36
36
|
|
|
37
37
|
const editing = editingId ? tasks[editingId] : null
|
|
38
|
-
const
|
|
38
|
+
const agentList = Object.values(agents)
|
|
39
39
|
|
|
40
40
|
useEffect(() => {
|
|
41
41
|
if (open) {
|
|
@@ -50,7 +50,7 @@ export function TaskSheet() {
|
|
|
50
50
|
} else {
|
|
51
51
|
setTitle('')
|
|
52
52
|
setDescription('')
|
|
53
|
-
setAgentId(
|
|
53
|
+
setAgentId(agentList[0]?.id || '')
|
|
54
54
|
setImages([])
|
|
55
55
|
setCwd('')
|
|
56
56
|
setFile(null)
|
|
@@ -58,12 +58,12 @@ export function TaskSheet() {
|
|
|
58
58
|
}
|
|
59
59
|
}, [open, editingId])
|
|
60
60
|
|
|
61
|
-
// Update default agent when
|
|
61
|
+
// Update default agent when agents load (only if no agent selected yet)
|
|
62
62
|
useEffect(() => {
|
|
63
|
-
if (open && !editing && !agentId &&
|
|
64
|
-
setAgentId(
|
|
63
|
+
if (open && !editing && !agentId && agentList.length) {
|
|
64
|
+
setAgentId(agentList[0].id)
|
|
65
65
|
}
|
|
66
|
-
}, [open, editing, agentId,
|
|
66
|
+
}, [open, editing, agentId, agentList.length, agents])
|
|
67
67
|
|
|
68
68
|
const onClose = () => {
|
|
69
69
|
setOpen(false)
|
|
@@ -145,7 +145,7 @@ export function TaskSheet() {
|
|
|
145
145
|
{editing ? 'Edit Task' : 'New Task'}
|
|
146
146
|
</h2>
|
|
147
147
|
<p className="text-[14px] text-text-3">
|
|
148
|
-
{editing ? `Status: ${editing.status}` : 'Create a task and assign an
|
|
148
|
+
{editing ? `Status: ${editing.status}` : 'Create a task and assign an agent'}
|
|
149
149
|
</p>
|
|
150
150
|
</div>
|
|
151
151
|
|
|
@@ -166,7 +166,7 @@ export function TaskSheet() {
|
|
|
166
166
|
<textarea
|
|
167
167
|
value={description}
|
|
168
168
|
onChange={(e) => setDescription(e.target.value)}
|
|
169
|
-
placeholder="Detailed task instructions for the
|
|
169
|
+
placeholder="Detailed task instructions for the agent..."
|
|
170
170
|
rows={4}
|
|
171
171
|
className={`${inputClass} resize-y min-h-[100px]`}
|
|
172
172
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -206,10 +206,10 @@ export function TaskSheet() {
|
|
|
206
206
|
</div>
|
|
207
207
|
|
|
208
208
|
<div className="mb-8">
|
|
209
|
-
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
210
|
-
{
|
|
209
|
+
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Agent</label>
|
|
210
|
+
{agentList.length > 0 ? (
|
|
211
211
|
<div className="flex flex-wrap gap-2">
|
|
212
|
-
{
|
|
212
|
+
{agentList.map((p) => (
|
|
213
213
|
<button
|
|
214
214
|
key={p.id}
|
|
215
215
|
onClick={() => setAgentId(p.id)}
|
|
@@ -224,7 +224,7 @@ export function TaskSheet() {
|
|
|
224
224
|
))}
|
|
225
225
|
</div>
|
|
226
226
|
) : (
|
|
227
|
-
<p className="text-[13px] text-text-3">No
|
|
227
|
+
<p className="text-[13px] text-text-3">No agents configured. Create one in Agents first.</p>
|
|
228
228
|
)}
|
|
229
229
|
</div>
|
|
230
230
|
|
|
@@ -4,6 +4,42 @@ import { useCallback, useRef, useState } from 'react'
|
|
|
4
4
|
|
|
5
5
|
export type ContinuousSpeechState = 'idle' | 'listening' | 'cooldown' | 'waitingForResponse'
|
|
6
6
|
|
|
7
|
+
interface SpeechRecognitionResult {
|
|
8
|
+
isFinal: boolean
|
|
9
|
+
[index: number]: { transcript: string }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface SpeechRecognitionResultList {
|
|
13
|
+
readonly length: number
|
|
14
|
+
[index: number]: SpeechRecognitionResult
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SpeechRecognitionEvent {
|
|
18
|
+
resultIndex: number
|
|
19
|
+
results: SpeechRecognitionResultList
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SpeechRecognitionErrorEvent {
|
|
23
|
+
error: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface SpeechRecognitionInstance {
|
|
27
|
+
continuous: boolean
|
|
28
|
+
interimResults: boolean
|
|
29
|
+
maxAlternatives: number
|
|
30
|
+
lang: string
|
|
31
|
+
onresult: ((e: SpeechRecognitionEvent) => void) | null
|
|
32
|
+
onerror: ((e: SpeechRecognitionErrorEvent) => void) | null
|
|
33
|
+
onend: (() => void) | null
|
|
34
|
+
start(): void
|
|
35
|
+
stop(): void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface WindowWithSpeechRecognition {
|
|
39
|
+
SpeechRecognition?: new () => SpeechRecognitionInstance
|
|
40
|
+
webkitSpeechRecognition?: new () => SpeechRecognitionInstance
|
|
41
|
+
}
|
|
42
|
+
|
|
7
43
|
interface UseContinuousSpeechOptions {
|
|
8
44
|
lang?: string
|
|
9
45
|
silenceDelayMs?: number
|
|
@@ -16,7 +52,7 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
16
52
|
const [transcript, setTranscript] = useState('')
|
|
17
53
|
const [interimText, setInterimText] = useState('')
|
|
18
54
|
|
|
19
|
-
const recogRef = useRef<
|
|
55
|
+
const recogRef = useRef<SpeechRecognitionInstance | null>(null)
|
|
20
56
|
const silenceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
21
57
|
const activeRef = useRef(false)
|
|
22
58
|
const accumulatedRef = useRef('')
|
|
@@ -29,7 +65,8 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
29
65
|
}
|
|
30
66
|
|
|
31
67
|
const startRecognition = useCallback(() => {
|
|
32
|
-
const
|
|
68
|
+
const w = window as unknown as WindowWithSpeechRecognition
|
|
69
|
+
const SR = w.SpeechRecognition || w.webkitSpeechRecognition
|
|
33
70
|
if (!SR) return
|
|
34
71
|
|
|
35
72
|
if (recogRef.current) {
|
|
@@ -42,7 +79,7 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
42
79
|
recog.maxAlternatives = 1
|
|
43
80
|
recog.lang = lang || navigator.language || 'en-US'
|
|
44
81
|
|
|
45
|
-
recog.onresult = (e:
|
|
82
|
+
recog.onresult = (e: SpeechRecognitionEvent) => {
|
|
46
83
|
clearSilenceTimer()
|
|
47
84
|
let interim = ''
|
|
48
85
|
let final = ''
|
|
@@ -77,7 +114,7 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
77
114
|
}
|
|
78
115
|
}
|
|
79
116
|
|
|
80
|
-
recog.onerror = (e:
|
|
117
|
+
recog.onerror = (e: SpeechRecognitionErrorEvent) => {
|
|
81
118
|
// 'no-speech' is normal during silence; 'aborted' when stopping intentionally
|
|
82
119
|
if (e.error === 'no-speech' || e.error === 'aborted') return
|
|
83
120
|
console.warn('[continuous-speech] error:', e.error)
|
|
@@ -138,7 +175,7 @@ export function useContinuousSpeech(options: UseContinuousSpeechOptions) {
|
|
|
138
175
|
}, [startRecognition])
|
|
139
176
|
|
|
140
177
|
const supported = typeof window !== 'undefined' &&
|
|
141
|
-
!!((window as
|
|
178
|
+
!!((window as unknown as WindowWithSpeechRecognition).SpeechRecognition || (window as unknown as WindowWithSpeechRecognition).webkitSpeechRecognition)
|
|
142
179
|
|
|
143
180
|
return { state, transcript, interimText, start, stop, pause, resume, supported }
|
|
144
181
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import { useWs } from './use-ws'
|
|
6
|
+
|
|
7
|
+
/** Call an OpenClaw gateway RPC method via the proxy route. */
|
|
8
|
+
export function useOpenClawRpc<T = unknown>(method: string | null, params?: unknown) {
|
|
9
|
+
const [data, setData] = useState<T | null>(null)
|
|
10
|
+
const [loading, setLoading] = useState(false)
|
|
11
|
+
const [error, setError] = useState<string | null>(null)
|
|
12
|
+
const paramsRef = useRef(params)
|
|
13
|
+
paramsRef.current = params
|
|
14
|
+
|
|
15
|
+
const fetch = useCallback(async () => {
|
|
16
|
+
if (!method) return
|
|
17
|
+
setLoading(true)
|
|
18
|
+
setError(null)
|
|
19
|
+
try {
|
|
20
|
+
const res = await api<{ ok: boolean; result: T; error?: string }>('POST', '/openclaw/gateway', {
|
|
21
|
+
method,
|
|
22
|
+
params: paramsRef.current,
|
|
23
|
+
})
|
|
24
|
+
if (res.error) {
|
|
25
|
+
setError(res.error)
|
|
26
|
+
} else {
|
|
27
|
+
setData(res.result)
|
|
28
|
+
}
|
|
29
|
+
} catch (err: unknown) {
|
|
30
|
+
setError(err instanceof Error ? err.message : String(err))
|
|
31
|
+
} finally {
|
|
32
|
+
setLoading(false)
|
|
33
|
+
}
|
|
34
|
+
}, [method])
|
|
35
|
+
|
|
36
|
+
useEffect(() => { fetch() }, [fetch])
|
|
37
|
+
|
|
38
|
+
return { data, loading, error, refetch: fetch }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Subscribe to an OpenClaw event topic via the WS hub. */
|
|
42
|
+
export function useOpenClawEvent(topic: string, handler: () => void) {
|
|
43
|
+
useWs(`openclaw:${topic}`, handler)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Check gateway connection status. */
|
|
47
|
+
export function useOpenClawConnected() {
|
|
48
|
+
const [connected, setConnected] = useState(false)
|
|
49
|
+
|
|
50
|
+
const check = useCallback(async () => {
|
|
51
|
+
try {
|
|
52
|
+
const res = await api<{ connected: boolean }>('GET', '/openclaw/gateway')
|
|
53
|
+
setConnected(res.connected)
|
|
54
|
+
} catch {
|
|
55
|
+
setConnected(false)
|
|
56
|
+
}
|
|
57
|
+
}, [])
|
|
58
|
+
|
|
59
|
+
useEffect(() => { check() }, [check])
|
|
60
|
+
useWs('openclaw:agents', check)
|
|
61
|
+
|
|
62
|
+
return connected
|
|
63
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
let ctx: AudioContext | null = null
|
|
2
|
+
|
|
3
|
+
function ensureCtx(): AudioContext | null {
|
|
4
|
+
if (!ctx) {
|
|
5
|
+
try { ctx = new AudioContext() } catch { return null }
|
|
6
|
+
}
|
|
7
|
+
if (ctx.state === 'suspended') ctx.resume()
|
|
8
|
+
return ctx
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function tone(freq: number, duration: number, type: OscillatorType = 'sine', delay = 0) {
|
|
12
|
+
const c = ensureCtx()
|
|
13
|
+
if (!c) return
|
|
14
|
+
const osc = c.createOscillator()
|
|
15
|
+
const gain = c.createGain()
|
|
16
|
+
osc.type = type
|
|
17
|
+
osc.frequency.value = freq
|
|
18
|
+
gain.gain.setValueAtTime(0.12, c.currentTime + delay)
|
|
19
|
+
gain.gain.exponentialRampToValueAtTime(0.001, c.currentTime + delay + duration / 1000)
|
|
20
|
+
osc.connect(gain)
|
|
21
|
+
gain.connect(c.destination)
|
|
22
|
+
osc.start(c.currentTime + delay)
|
|
23
|
+
osc.stop(c.currentTime + delay + duration / 1000)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Two ascending tones: C5 → E5 */
|
|
27
|
+
export function playStreamStart() {
|
|
28
|
+
tone(523, 80, 'sine', 0)
|
|
29
|
+
tone(659, 80, 'sine', 0.09)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Two descending tones: E5 → C5 */
|
|
33
|
+
export function playStreamEnd() {
|
|
34
|
+
tone(659, 80, 'sine', 0)
|
|
35
|
+
tone(523, 80, 'sine', 0.09)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Single ding: A5 */
|
|
39
|
+
export function playToolComplete() {
|
|
40
|
+
tone(880, 120, 'triangle')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Low buzz: A3 */
|
|
44
|
+
export function playError() {
|
|
45
|
+
tone(220, 200, 'square')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const LS_KEY = 'sc_sound_notifications'
|
|
49
|
+
|
|
50
|
+
export function getSoundEnabled(): boolean {
|
|
51
|
+
if (typeof window === 'undefined') return false
|
|
52
|
+
return localStorage.getItem(LS_KEY) === '1'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function setSoundEnabled(v: boolean) {
|
|
56
|
+
if (typeof window === 'undefined') return
|
|
57
|
+
localStorage.setItem(LS_KEY, v ? '1' : '0')
|
|
58
|
+
}
|