@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.
Files changed (113) hide show
  1. package/README.md +8 -2
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +5 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +23 -5
  6. package/src/app/api/agents/trash/route.ts +44 -0
  7. package/src/app/api/connectors/[id]/route.ts +7 -4
  8. package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
  9. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  10. package/src/app/api/openclaw/approvals/route.ts +46 -0
  11. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  12. package/src/app/api/openclaw/cron/route.ts +52 -0
  13. package/src/app/api/openclaw/directory/route.ts +4 -3
  14. package/src/app/api/openclaw/discover/route.ts +3 -2
  15. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  16. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  17. package/src/app/api/openclaw/gateway/route.ts +72 -0
  18. package/src/app/api/openclaw/history/route.ts +109 -0
  19. package/src/app/api/openclaw/media/route.ts +53 -0
  20. package/src/app/api/openclaw/models/route.ts +12 -0
  21. package/src/app/api/openclaw/permissions/route.ts +39 -0
  22. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  23. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  24. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  25. package/src/app/api/openclaw/skills/route.ts +82 -0
  26. package/src/app/api/openclaw/sync/route.ts +3 -2
  27. package/src/app/api/projects/[id]/route.ts +1 -1
  28. package/src/app/api/projects/route.ts +1 -1
  29. package/src/app/api/secrets/[id]/route.ts +1 -1
  30. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  31. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  32. package/src/app/api/sessions/[id]/messages/route.ts +18 -1
  33. package/src/app/api/sessions/[id]/route.ts +12 -3
  34. package/src/app/api/sessions/route.ts +6 -2
  35. package/src/app/globals.css +14 -0
  36. package/src/app/layout.tsx +5 -20
  37. package/src/cli/index.js +33 -1
  38. package/src/cli/spec.js +40 -0
  39. package/src/components/agents/agent-avatar.tsx +45 -0
  40. package/src/components/agents/agent-card.tsx +19 -5
  41. package/src/components/agents/agent-chat-list.tsx +31 -24
  42. package/src/components/agents/agent-files-editor.tsx +185 -0
  43. package/src/components/agents/agent-list.tsx +82 -3
  44. package/src/components/agents/agent-sheet.tsx +31 -0
  45. package/src/components/agents/cron-job-form.tsx +137 -0
  46. package/src/components/agents/exec-config-panel.tsx +147 -0
  47. package/src/components/agents/inspector-panel.tsx +310 -0
  48. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  49. package/src/components/agents/permission-preset-selector.tsx +79 -0
  50. package/src/components/agents/personality-builder.tsx +111 -0
  51. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  52. package/src/components/agents/skill-install-dialog.tsx +102 -0
  53. package/src/components/agents/trash-list.tsx +109 -0
  54. package/src/components/chat/chat-area.tsx +14 -2
  55. package/src/components/chat/chat-header.tsx +168 -4
  56. package/src/components/chat/chat-preview-panel.tsx +113 -0
  57. package/src/components/chat/exec-approval-card.tsx +89 -0
  58. package/src/components/chat/message-bubble.tsx +218 -36
  59. package/src/components/chat/message-list.tsx +135 -31
  60. package/src/components/chat/streaming-bubble.tsx +59 -10
  61. package/src/components/chat/suggestions-bar.tsx +74 -0
  62. package/src/components/chat/thinking-indicator.tsx +20 -6
  63. package/src/components/chat/tool-call-bubble.tsx +89 -16
  64. package/src/components/chat/tool-request-banner.tsx +20 -2
  65. package/src/components/chat/trace-block.tsx +103 -0
  66. package/src/components/projects/project-list.tsx +1 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  68. package/src/components/shared/avatar.tsx +13 -2
  69. package/src/components/shared/settings/settings-page.tsx +1 -0
  70. package/src/components/tasks/task-board.tsx +1 -1
  71. package/src/components/tasks/task-sheet.tsx +12 -12
  72. package/src/hooks/use-continuous-speech.ts +42 -5
  73. package/src/hooks/use-openclaw-gateway.ts +63 -0
  74. package/src/lib/notification-sounds.ts +58 -0
  75. package/src/lib/personality-parser.ts +97 -0
  76. package/src/lib/providers/openclaw.ts +17 -2
  77. package/src/lib/runtime-loop.ts +2 -2
  78. package/src/lib/server/chat-execution.ts +44 -2
  79. package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
  80. package/src/lib/server/connectors/bluebubbles.ts +5 -2
  81. package/src/lib/server/connectors/googlechat.ts +14 -10
  82. package/src/lib/server/connectors/manager.ts +37 -15
  83. package/src/lib/server/connectors/openclaw.ts +1 -0
  84. package/src/lib/server/daemon-state.ts +11 -0
  85. package/src/lib/server/main-agent-loop.ts +2 -3
  86. package/src/lib/server/main-session.ts +21 -0
  87. package/src/lib/server/openclaw-config-sync.ts +107 -0
  88. package/src/lib/server/openclaw-exec-config.ts +52 -0
  89. package/src/lib/server/openclaw-gateway.ts +291 -0
  90. package/src/lib/server/openclaw-history-merge.ts +36 -0
  91. package/src/lib/server/openclaw-models.ts +56 -0
  92. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  93. package/src/lib/server/openclaw-sync.ts +11 -10
  94. package/src/lib/server/queue.ts +2 -1
  95. package/src/lib/server/session-tools/connector.ts +4 -4
  96. package/src/lib/server/session-tools/delegate.ts +19 -3
  97. package/src/lib/server/session-tools/file.ts +20 -20
  98. package/src/lib/server/session-tools/index.ts +2 -2
  99. package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
  100. package/src/lib/server/session-tools/sandbox.ts +2 -2
  101. package/src/lib/server/session-tools/search-providers.ts +13 -6
  102. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  103. package/src/lib/server/session-tools/shell.ts +1 -1
  104. package/src/lib/server/session-tools/web.ts +8 -8
  105. package/src/lib/server/storage.ts +62 -11
  106. package/src/lib/server/stream-agent-chat.ts +24 -4
  107. package/src/lib/server/suggestions.ts +20 -0
  108. package/src/lib/server/ws-hub.ts +14 -0
  109. package/src/stores/use-app-store.ts +53 -1
  110. package/src/stores/use-approval-store.ts +78 -0
  111. package/src/stores/use-chat-store.ts +153 -5
  112. package/src/types/index.ts +127 -1
  113. 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 function AiAvatar({ size = 'md' }: { size?: 'sm' | 'md' }) {
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" />
@@ -32,6 +32,7 @@ export function SettingsPage() {
32
32
  loadSettings()
33
33
  loadSecrets()
34
34
  loadAgents()
35
+ // eslint-disable-next-line react-hooks/exhaustive-deps
35
36
  }, [])
36
37
 
37
38
  const credList = Object.values(credentials)
@@ -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 orchestrators to run them sequentially</p>
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 orchestrators = Object.values(agents).filter((p) => p.isOrchestrator)
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(orchestrators[0]?.id || '')
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 orchestrators load (only if no agent selected yet)
61
+ // Update default agent when agents load (only if no agent selected yet)
62
62
  useEffect(() => {
63
- if (open && !editing && !agentId && orchestrators.length) {
64
- setAgentId(orchestrators[0].id)
63
+ if (open && !editing && !agentId && agentList.length) {
64
+ setAgentId(agentList[0].id)
65
65
  }
66
- }, [open, editing, agentId, orchestrators.length, agents])
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 orchestrator'}
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 orchestrator..."
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">Orchestrator</label>
210
- {orchestrators.length > 0 ? (
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
- {orchestrators.map((p) => (
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 orchestrator agents configured. Create one in Agents first.</p>
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<any>(null)
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 SR = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition
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: any) => {
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: any) => {
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 any).SpeechRecognition || (window as any).webkitSpeechRecognition)
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
+ }