@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.
Files changed (144) hide show
  1. package/README.md +13 -2
  2. package/next.config.ts +8 -0
  3. package/package.json +2 -1
  4. package/src/app/api/agents/[id]/route.ts +20 -21
  5. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  6. package/src/app/api/agents/route.ts +3 -2
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/connectors/[id]/route.ts +10 -3
  9. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  10. package/src/app/api/connectors/route.ts +6 -3
  11. package/src/app/api/credentials/[id]/route.ts +2 -1
  12. package/src/app/api/credentials/route.ts +2 -2
  13. package/src/app/api/documents/route.ts +2 -2
  14. package/src/app/api/files/serve/route.ts +8 -0
  15. package/src/app/api/knowledge/[id]/route.ts +5 -4
  16. package/src/app/api/knowledge/upload/route.ts +2 -2
  17. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  18. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  19. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  20. package/src/app/api/mcp-servers/route.ts +2 -2
  21. package/src/app/api/memory/[id]/route.ts +9 -8
  22. package/src/app/api/memory/route.ts +2 -2
  23. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  24. package/src/app/api/openclaw/directory/route.ts +26 -0
  25. package/src/app/api/openclaw/discover/route.ts +61 -0
  26. package/src/app/api/openclaw/sync/route.ts +30 -0
  27. package/src/app/api/orchestrator/run/route.ts +2 -2
  28. package/src/app/api/projects/[id]/route.ts +55 -0
  29. package/src/app/api/projects/route.ts +27 -0
  30. package/src/app/api/providers/[id]/models/route.ts +2 -1
  31. package/src/app/api/providers/[id]/route.ts +13 -15
  32. package/src/app/api/providers/route.ts +2 -2
  33. package/src/app/api/schedules/[id]/route.ts +16 -18
  34. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  35. package/src/app/api/schedules/route.ts +2 -2
  36. package/src/app/api/secrets/[id]/route.ts +16 -17
  37. package/src/app/api/secrets/route.ts +2 -2
  38. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  39. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  40. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  41. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  42. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  43. package/src/app/api/sessions/[id]/route.ts +2 -1
  44. package/src/app/api/sessions/route.ts +2 -2
  45. package/src/app/api/skills/[id]/route.ts +23 -21
  46. package/src/app/api/skills/import/route.ts +2 -2
  47. package/src/app/api/skills/route.ts +2 -2
  48. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  49. package/src/app/api/tasks/[id]/route.ts +6 -5
  50. package/src/app/api/tasks/route.ts +2 -2
  51. package/src/app/api/tts/stream/route.ts +48 -0
  52. package/src/app/api/upload/route.ts +2 -2
  53. package/src/app/api/uploads/[filename]/route.ts +4 -1
  54. package/src/app/api/webhooks/[id]/route.ts +29 -31
  55. package/src/app/api/webhooks/route.ts +2 -2
  56. package/src/app/page.tsx +3 -24
  57. package/src/cli/index.js +28 -0
  58. package/src/cli/index.ts +1 -1
  59. package/src/cli/spec.js +2 -0
  60. package/src/components/agents/agent-list.tsx +3 -1
  61. package/src/components/agents/agent-sheet.tsx +116 -14
  62. package/src/components/chat/chat-area.tsx +27 -4
  63. package/src/components/chat/chat-header.tsx +141 -29
  64. package/src/components/chat/tool-call-bubble.tsx +9 -3
  65. package/src/components/chat/voice-overlay.tsx +80 -0
  66. package/src/components/connectors/connector-list.tsx +6 -2
  67. package/src/components/connectors/connector-sheet.tsx +31 -7
  68. package/src/components/layout/app-layout.tsx +47 -25
  69. package/src/components/projects/project-list.tsx +122 -0
  70. package/src/components/projects/project-sheet.tsx +135 -0
  71. package/src/components/schedules/schedule-list.tsx +3 -1
  72. package/src/components/sessions/new-session-sheet.tsx +6 -6
  73. package/src/components/sessions/session-card.tsx +1 -1
  74. package/src/components/sessions/session-list.tsx +7 -7
  75. package/src/components/shared/connector-platform-icon.tsx +4 -0
  76. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  77. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  78. package/src/components/shared/settings/section-web-search.tsx +56 -0
  79. package/src/components/shared/settings/settings-page.tsx +73 -0
  80. package/src/components/skills/skill-list.tsx +2 -1
  81. package/src/components/tasks/task-list.tsx +5 -2
  82. package/src/hooks/use-continuous-speech.ts +144 -0
  83. package/src/hooks/use-view-router.ts +52 -0
  84. package/src/hooks/use-voice-conversation.ts +80 -0
  85. package/src/lib/id.ts +6 -0
  86. package/src/lib/projects.ts +13 -0
  87. package/src/lib/provider-sets.ts +5 -0
  88. package/src/lib/providers/anthropic.ts +14 -1
  89. package/src/lib/providers/index.ts +6 -0
  90. package/src/lib/providers/ollama.ts +9 -1
  91. package/src/lib/providers/openai.ts +9 -1
  92. package/src/lib/providers/openclaw.ts +11 -0
  93. package/src/lib/server/api-routes.test.ts +5 -6
  94. package/src/lib/server/build-llm.ts +17 -4
  95. package/src/lib/server/chat-execution.ts +38 -4
  96. package/src/lib/server/collection-helpers.ts +54 -0
  97. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  98. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  99. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  100. package/src/lib/server/connectors/googlechat.ts +46 -7
  101. package/src/lib/server/connectors/manager.ts +392 -3
  102. package/src/lib/server/connectors/media.ts +2 -2
  103. package/src/lib/server/connectors/openclaw.ts +64 -0
  104. package/src/lib/server/connectors/pairing.test.ts +99 -0
  105. package/src/lib/server/connectors/pairing.ts +256 -0
  106. package/src/lib/server/connectors/signal.ts +1 -0
  107. package/src/lib/server/connectors/teams.ts +5 -5
  108. package/src/lib/server/connectors/types.ts +10 -0
  109. package/src/lib/server/execution-log.ts +3 -3
  110. package/src/lib/server/heartbeat-service.ts +1 -1
  111. package/src/lib/server/knowledge-db.test.ts +2 -33
  112. package/src/lib/server/main-agent-loop.ts +6 -6
  113. package/src/lib/server/memory-db.ts +6 -6
  114. package/src/lib/server/openclaw-approvals.ts +105 -0
  115. package/src/lib/server/openclaw-sync.ts +496 -0
  116. package/src/lib/server/orchestrator-lg.ts +30 -9
  117. package/src/lib/server/orchestrator.ts +4 -4
  118. package/src/lib/server/process-manager.ts +2 -2
  119. package/src/lib/server/queue.ts +22 -10
  120. package/src/lib/server/scheduler.ts +2 -2
  121. package/src/lib/server/session-mailbox.ts +2 -2
  122. package/src/lib/server/session-run-manager.ts +2 -2
  123. package/src/lib/server/session-tools/connector.ts +51 -4
  124. package/src/lib/server/session-tools/crud.ts +3 -3
  125. package/src/lib/server/session-tools/delegate.ts +3 -3
  126. package/src/lib/server/session-tools/file.ts +176 -3
  127. package/src/lib/server/session-tools/index.ts +2 -0
  128. package/src/lib/server/session-tools/memory.ts +2 -2
  129. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  130. package/src/lib/server/session-tools/sandbox.ts +33 -0
  131. package/src/lib/server/session-tools/search-providers.ts +270 -0
  132. package/src/lib/server/session-tools/session-info.ts +2 -2
  133. package/src/lib/server/session-tools/web.ts +47 -66
  134. package/src/lib/server/storage.ts +12 -0
  135. package/src/lib/server/stream-agent-chat.ts +29 -0
  136. package/src/lib/server/task-result.test.ts +44 -0
  137. package/src/lib/server/task-result.ts +14 -0
  138. package/src/lib/tool-definitions.ts +5 -3
  139. package/src/lib/tts-stream.ts +130 -0
  140. package/src/lib/view-routes.ts +28 -0
  141. package/src/proxy.ts +3 -0
  142. package/src/stores/use-app-store.ts +28 -1
  143. package/src/stores/use-chat-store.ts +9 -1
  144. 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 Session'
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 Session</h2>
178
- <p className="text-[14px] text-text-3">Configure your AI session</p>
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
- Session Name
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 session directory.</p>
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 Session
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 session"
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 sessions yet</p>
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 Session
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} session${filtered.length === 1 ? '' : 's'}?`)) return
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 sessions"
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 sessions"
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 session' : 'Pin session'}
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} sessions{search ? ` matching "${search}"` : ''}
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 sessions.')
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 &quot;{provider}&quot; or &quot;{provider}_api_key&quot; 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).sort((a, b) => b.updatedAt - a.updatedAt),
32
- [tasks],
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
+ }