@swarmclawai/swarmclaw 0.6.0 → 0.6.2
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 +15 -2
- package/bin/server-cmd.js +1 -0
- package/package.json +2 -1
- package/src/app/api/canvas/[sessionId]/route.ts +31 -0
- package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
- package/src/app/api/connectors/[id]/route.ts +1 -0
- package/src/app/api/connectors/route.ts +2 -1
- package/src/app/api/files/open/route.ts +43 -0
- package/src/app/api/search/route.ts +9 -7
- package/src/app/api/sessions/[id]/messages/route.ts +70 -2
- package/src/app/api/sessions/[id]/route.ts +4 -0
- package/src/app/api/tasks/metrics/route.ts +101 -0
- package/src/app/api/tasks/route.ts +17 -2
- package/src/app/api/tts/route.ts +3 -2
- package/src/app/api/tts/stream/route.ts +3 -2
- package/src/app/api/uploads/[filename]/route.ts +19 -34
- package/src/app/api/uploads/route.ts +94 -0
- package/src/app/globals.css +5 -0
- package/src/cli/index.js +16 -1
- package/src/cli/spec.js +26 -0
- package/src/components/agents/agent-card.tsx +3 -3
- package/src/components/agents/agent-chat-list.tsx +29 -6
- package/src/components/agents/agent-sheet.tsx +66 -4
- package/src/components/agents/inspector-panel.tsx +81 -6
- package/src/components/agents/openclaw-skills-panel.tsx +32 -3
- package/src/components/agents/personality-builder.tsx +42 -14
- package/src/components/agents/soul-library-picker.tsx +89 -0
- package/src/components/canvas/canvas-panel.tsx +96 -0
- package/src/components/chat/activity-moment.tsx +8 -4
- package/src/components/chat/chat-area.tsx +46 -22
- package/src/components/chat/chat-header.tsx +455 -286
- package/src/components/chat/chat-preview-panel.tsx +1 -2
- package/src/components/chat/delegation-banner.tsx +371 -0
- package/src/components/chat/file-path-chip.tsx +23 -2
- package/src/components/chat/heartbeat-history-panel.tsx +269 -0
- package/src/components/chat/message-bubble.tsx +315 -25
- package/src/components/chat/message-list.tsx +180 -7
- package/src/components/chat/streaming-bubble.tsx +68 -1
- package/src/components/chat/tool-call-bubble.tsx +45 -3
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/chatroom-list.tsx +8 -1
- package/src/components/chatrooms/chatroom-message.tsx +8 -3
- package/src/components/chatrooms/chatroom-view.tsx +3 -3
- package/src/components/connectors/connector-list.tsx +168 -90
- package/src/components/connectors/connector-sheet.tsx +68 -16
- package/src/components/home/home-view.tsx +1 -1
- package/src/components/input/chat-input.tsx +28 -2
- package/src/components/layout/app-layout.tsx +19 -2
- package/src/components/projects/project-detail.tsx +1 -1
- package/src/components/schedules/schedule-sheet.tsx +260 -127
- package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/chatroom-picker-list.tsx +61 -0
- package/src/components/shared/connector-platform-icon.tsx +51 -4
- package/src/components/shared/icon-button.tsx +16 -2
- package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/shared/search-dialog.tsx +17 -10
- package/src/components/shared/settings/section-embedding.tsx +48 -13
- package/src/components/shared/settings/section-orchestrator.tsx +46 -15
- package/src/components/shared/settings/section-storage.tsx +206 -0
- package/src/components/shared/settings/section-user-preferences.tsx +18 -0
- package/src/components/shared/settings/section-voice.tsx +42 -21
- package/src/components/shared/settings/section-web-search.tsx +30 -6
- package/src/components/shared/settings/settings-page.tsx +3 -1
- package/src/components/shared/settings/storage-browser.tsx +259 -0
- package/src/components/tasks/task-card.tsx +14 -1
- package/src/components/tasks/task-sheet.tsx +328 -3
- package/src/components/usage/metrics-dashboard.tsx +90 -6
- package/src/hooks/use-continuous-speech.ts +10 -4
- package/src/hooks/use-voice-conversation.ts +53 -10
- package/src/hooks/use-ws.ts +4 -2
- package/src/lib/providers/anthropic.ts +13 -7
- package/src/lib/providers/index.ts +1 -0
- package/src/lib/providers/openai.ts +13 -7
- package/src/lib/server/chat-execution.ts +51 -11
- package/src/lib/server/chatroom-helpers.ts +146 -0
- package/src/lib/server/connectors/manager.ts +218 -7
- package/src/lib/server/heartbeat-service.ts +8 -1
- package/src/lib/server/main-agent-loop.ts +1 -1
- package/src/lib/server/memory-consolidation.ts +15 -2
- package/src/lib/server/memory-db.ts +134 -6
- package/src/lib/server/mime.ts +51 -0
- package/src/lib/server/openclaw-gateway.ts +2 -2
- package/src/lib/server/orchestrator-lg.ts +2 -0
- package/src/lib/server/orchestrator.ts +5 -2
- package/src/lib/server/playwright-proxy.mjs +2 -3
- package/src/lib/server/prompt-runtime-context.ts +53 -0
- package/src/lib/server/queue.ts +52 -7
- package/src/lib/server/session-tools/canvas.ts +67 -0
- package/src/lib/server/session-tools/connector.ts +83 -9
- package/src/lib/server/session-tools/crud.ts +21 -0
- package/src/lib/server/session-tools/delegate.ts +68 -4
- package/src/lib/server/session-tools/git.ts +71 -0
- package/src/lib/server/session-tools/http.ts +57 -0
- package/src/lib/server/session-tools/index.ts +8 -0
- package/src/lib/server/session-tools/memory.ts +1 -0
- package/src/lib/server/session-tools/search-providers.ts +16 -8
- package/src/lib/server/session-tools/subagent.ts +106 -0
- package/src/lib/server/session-tools/web.ts +115 -4
- package/src/lib/server/stream-agent-chat.ts +32 -10
- package/src/lib/server/task-mention.ts +41 -0
- package/src/lib/sessions.ts +10 -0
- package/src/lib/soul-library.ts +103 -0
- package/src/lib/task-dedupe.ts +26 -0
- package/src/lib/tool-definitions.ts +2 -0
- package/src/lib/tts.ts +2 -2
- package/src/stores/use-app-store.ts +5 -1
- package/src/stores/use-chat-store.ts +65 -2
- package/src/types/index.ts +32 -2
|
@@ -5,7 +5,6 @@ import { useAppStore } from '@/stores/use-app-store'
|
|
|
5
5
|
import { createSchedule, updateSchedule, deleteSchedule } from '@/lib/schedules'
|
|
6
6
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
7
7
|
import { AgentPickerList } from '@/components/shared/agent-picker-list'
|
|
8
|
-
import { SheetFooter } from '@/components/shared/sheet-footer'
|
|
9
8
|
import { inputClass } from '@/components/shared/form-styles'
|
|
10
9
|
import type { ScheduleType, ScheduleStatus } from '@/types'
|
|
11
10
|
import cronstrue from 'cronstrue'
|
|
@@ -18,11 +17,10 @@ const CRON_PRESETS = [
|
|
|
18
17
|
{ label: 'Weekly Mon 9am', cron: '0 9 * * 1' },
|
|
19
18
|
]
|
|
20
19
|
|
|
21
|
-
function
|
|
20
|
+
async function getNextRunsAsync(cron: string, count: number = 3): Promise<Date[]> {
|
|
22
21
|
try {
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const interval = parseExpression(cron)
|
|
22
|
+
const { CronExpressionParser } = await import('cron-parser')
|
|
23
|
+
const interval = CronExpressionParser.parse(cron)
|
|
26
24
|
const runs: Date[] = []
|
|
27
25
|
for (let i = 0; i < count; i++) {
|
|
28
26
|
runs.push(interval.next().toDate())
|
|
@@ -46,6 +44,9 @@ function formatDate(d: Date): string {
|
|
|
46
44
|
' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
47
45
|
}
|
|
48
46
|
|
|
47
|
+
const STEPS = ['What', 'When', 'Review'] as const
|
|
48
|
+
type Step = 0 | 1 | 2
|
|
49
|
+
|
|
49
50
|
export function ScheduleSheet() {
|
|
50
51
|
const open = useAppStore((s) => s.scheduleSheetOpen)
|
|
51
52
|
const setOpen = useAppStore((s) => s.setScheduleSheetOpen)
|
|
@@ -56,6 +57,7 @@ export function ScheduleSheet() {
|
|
|
56
57
|
const agents = useAppStore((s) => s.agents)
|
|
57
58
|
const loadAgents = useAppStore((s) => s.loadAgents)
|
|
58
59
|
|
|
60
|
+
const [step, setStep] = useState<Step>(0)
|
|
59
61
|
const [name, setName] = useState('')
|
|
60
62
|
const [agentId, setAgentId] = useState('')
|
|
61
63
|
const [taskPrompt, setTaskPrompt] = useState('')
|
|
@@ -71,6 +73,7 @@ export function ScheduleSheet() {
|
|
|
71
73
|
useEffect(() => {
|
|
72
74
|
if (open) {
|
|
73
75
|
loadAgents()
|
|
76
|
+
setStep(0)
|
|
74
77
|
if (editing) {
|
|
75
78
|
setName(editing.name || '')
|
|
76
79
|
setAgentId(editing.agentId)
|
|
@@ -91,10 +94,14 @@ export function ScheduleSheet() {
|
|
|
91
94
|
setCustomCron(false)
|
|
92
95
|
}
|
|
93
96
|
}
|
|
97
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
94
98
|
}, [open, editingId])
|
|
95
99
|
|
|
96
100
|
const cronHuman = useMemo(() => formatCronHuman(cron), [cron])
|
|
97
|
-
const nextRuns =
|
|
101
|
+
const [nextRuns, setNextRuns] = useState<Date[]>([])
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
getNextRunsAsync(cron).then(setNextRuns)
|
|
104
|
+
}, [cron])
|
|
98
105
|
|
|
99
106
|
const onClose = () => {
|
|
100
107
|
setOpen(false)
|
|
@@ -129,161 +136,287 @@ export function ScheduleSheet() {
|
|
|
129
136
|
}
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
// Step validation
|
|
140
|
+
const step0Valid = name.trim().length > 0 && agentId.length > 0 && taskPrompt.trim().length > 0
|
|
141
|
+
const step1Valid = scheduleType === 'cron' ? cron.trim().length > 0 : intervalMs > 0
|
|
142
|
+
|
|
143
|
+
const selectedAgent = agentId ? agents[agentId] : null
|
|
144
|
+
|
|
132
145
|
return (
|
|
133
146
|
<BottomSheet open={open} onClose={onClose} wide>
|
|
134
|
-
<div className="mb-
|
|
147
|
+
<div className="mb-8">
|
|
135
148
|
<h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">
|
|
136
149
|
{editing ? 'Edit Schedule' : 'New Schedule'}
|
|
137
150
|
</h2>
|
|
138
151
|
<p className="text-[14px] text-text-3">Automate agent tasks on a schedule</p>
|
|
139
152
|
</div>
|
|
140
153
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<div className="mb-8">
|
|
147
|
-
<SectionLabel>Agent</SectionLabel>
|
|
148
|
-
<AgentPickerList
|
|
149
|
-
agents={agentList}
|
|
150
|
-
selected={agentId}
|
|
151
|
-
onSelect={(id) => setAgentId(id)}
|
|
152
|
-
showOrchBadge={true}
|
|
153
|
-
/>
|
|
154
|
-
</div>
|
|
155
|
-
|
|
156
|
-
<div className="mb-8">
|
|
157
|
-
<SectionLabel>Task Prompt</SectionLabel>
|
|
158
|
-
<textarea
|
|
159
|
-
value={taskPrompt}
|
|
160
|
-
onChange={(e) => setTaskPrompt(e.target.value)}
|
|
161
|
-
placeholder="What should the agent do when triggered?"
|
|
162
|
-
rows={4}
|
|
163
|
-
className={`${inputClass} resize-y min-h-[100px]`}
|
|
164
|
-
style={{ fontFamily: 'inherit' }}
|
|
165
|
-
/>
|
|
166
|
-
</div>
|
|
167
|
-
|
|
168
|
-
<div className="mb-8">
|
|
169
|
-
<SectionLabel>Schedule Type</SectionLabel>
|
|
170
|
-
<div className="grid grid-cols-3 gap-3">
|
|
171
|
-
{(['cron', 'interval', 'once'] as ScheduleType[]).map((t) => (
|
|
154
|
+
{/* Step indicator */}
|
|
155
|
+
<div className="flex items-center gap-2 mb-10">
|
|
156
|
+
{STEPS.map((label, i) => (
|
|
157
|
+
<div key={label} className="flex items-center gap-2">
|
|
158
|
+
{i > 0 && <div className={`w-8 h-px ${i <= step ? 'bg-accent-bright/40' : 'bg-white/[0.06]'}`} />}
|
|
172
159
|
<button
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
160
|
+
onClick={() => {
|
|
161
|
+
// Allow going back, but only forward if valid
|
|
162
|
+
if (i < step) setStep(i as Step)
|
|
163
|
+
else if (i === 1 && step === 0 && step0Valid) setStep(1)
|
|
164
|
+
else if (i === 2 && step === 1 && step1Valid) setStep(2)
|
|
165
|
+
}}
|
|
166
|
+
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-[8px] text-[12px] font-600 cursor-pointer transition-all border-none
|
|
167
|
+
${i === step
|
|
168
|
+
? 'bg-accent-soft text-accent-bright'
|
|
169
|
+
: i < step
|
|
170
|
+
? 'bg-white/[0.04] text-text-2'
|
|
171
|
+
: 'bg-transparent text-text-3/50'}`}
|
|
180
172
|
style={{ fontFamily: 'inherit' }}
|
|
181
173
|
>
|
|
182
|
-
{
|
|
174
|
+
<span className={`w-5 h-5 rounded-full text-[10px] font-700 flex items-center justify-center
|
|
175
|
+
${i === step
|
|
176
|
+
? 'bg-accent-bright text-white'
|
|
177
|
+
: i < step
|
|
178
|
+
? 'bg-emerald-400/20 text-emerald-400'
|
|
179
|
+
: 'bg-white/[0.06] text-text-3/50'}`}>
|
|
180
|
+
{i < step ? (
|
|
181
|
+
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round"><polyline points="20 6 9 17 4 12" /></svg>
|
|
182
|
+
) : (
|
|
183
|
+
i + 1
|
|
184
|
+
)}
|
|
185
|
+
</span>
|
|
186
|
+
{label}
|
|
183
187
|
</button>
|
|
184
|
-
|
|
185
|
-
|
|
188
|
+
</div>
|
|
189
|
+
))}
|
|
186
190
|
</div>
|
|
187
191
|
|
|
188
|
-
{
|
|
189
|
-
|
|
190
|
-
|
|
192
|
+
{/* Step 0: What */}
|
|
193
|
+
{step === 0 && (
|
|
194
|
+
<div>
|
|
195
|
+
<div className="mb-8">
|
|
196
|
+
<SectionLabel>Name</SectionLabel>
|
|
197
|
+
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Daily keyword research" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
198
|
+
</div>
|
|
191
199
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
className={
|
|
210
|
-
${customCron
|
|
211
|
-
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
212
|
-
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
200
|
+
<div className="mb-8">
|
|
201
|
+
<SectionLabel>Agent</SectionLabel>
|
|
202
|
+
<AgentPickerList
|
|
203
|
+
agents={agentList}
|
|
204
|
+
selected={agentId}
|
|
205
|
+
onSelect={(id) => setAgentId(id)}
|
|
206
|
+
showOrchBadge={true}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<div className="mb-8">
|
|
211
|
+
<SectionLabel>Task Prompt</SectionLabel>
|
|
212
|
+
<textarea
|
|
213
|
+
value={taskPrompt}
|
|
214
|
+
onChange={(e) => setTaskPrompt(e.target.value)}
|
|
215
|
+
placeholder="What should the agent do when triggered?"
|
|
216
|
+
rows={4}
|
|
217
|
+
className={`${inputClass} resize-y min-h-[100px]`}
|
|
213
218
|
style={{ fontFamily: 'inherit' }}
|
|
214
|
-
|
|
215
|
-
Custom
|
|
216
|
-
</button>
|
|
219
|
+
/>
|
|
217
220
|
</div>
|
|
221
|
+
</div>
|
|
222
|
+
)}
|
|
218
223
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
224
|
+
{/* Step 1: When */}
|
|
225
|
+
{step === 1 && (
|
|
226
|
+
<div>
|
|
227
|
+
<div className="mb-8">
|
|
228
|
+
<SectionLabel>Schedule Type</SectionLabel>
|
|
229
|
+
<div className="grid grid-cols-3 gap-3">
|
|
230
|
+
{(['cron', 'interval', 'once'] as ScheduleType[]).map((t) => (
|
|
231
|
+
<button
|
|
232
|
+
key={t}
|
|
233
|
+
onClick={() => setScheduleType(t)}
|
|
234
|
+
className={`py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
|
|
235
|
+
active:scale-[0.97] text-[14px] font-600 capitalize border
|
|
236
|
+
${scheduleType === t
|
|
237
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
238
|
+
: 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
|
|
239
|
+
style={{ fontFamily: 'inherit' }}
|
|
240
|
+
>
|
|
241
|
+
{t}
|
|
242
|
+
</button>
|
|
243
|
+
))}
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
223
246
|
|
|
224
|
-
{
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
247
|
+
{scheduleType === 'cron' && (
|
|
248
|
+
<div className="mb-8">
|
|
249
|
+
<SectionLabel>Schedule</SectionLabel>
|
|
250
|
+
|
|
251
|
+
{/* Preset buttons */}
|
|
252
|
+
<div className="flex flex-wrap gap-2 mb-4">
|
|
253
|
+
{CRON_PRESETS.map((p) => (
|
|
254
|
+
<button
|
|
255
|
+
key={p.cron}
|
|
256
|
+
onClick={() => { setCron(p.cron); setCustomCron(false) }}
|
|
257
|
+
className={`px-3.5 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
258
|
+
${cron === p.cron && !customCron
|
|
259
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
260
|
+
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
261
|
+
style={{ fontFamily: 'inherit' }}
|
|
262
|
+
>
|
|
263
|
+
{p.label}
|
|
264
|
+
</button>
|
|
235
265
|
))}
|
|
266
|
+
<button
|
|
267
|
+
onClick={() => setCustomCron(true)}
|
|
268
|
+
className={`px-3.5 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
|
|
269
|
+
${customCron
|
|
270
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
271
|
+
: 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
|
|
272
|
+
style={{ fontFamily: 'inherit' }}
|
|
273
|
+
>
|
|
274
|
+
Custom
|
|
275
|
+
</button>
|
|
236
276
|
</div>
|
|
237
|
-
)}
|
|
238
|
-
</div>
|
|
239
|
-
</div>
|
|
240
|
-
)}
|
|
241
277
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
278
|
+
{/* Custom cron input */}
|
|
279
|
+
{customCron && (
|
|
280
|
+
<input type="text" value={cron} onChange={(e) => setCron(e.target.value)} placeholder="0 * * * *" className={`${inputClass} font-mono text-[14px] mb-3`} />
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{/* Human-readable preview */}
|
|
284
|
+
<div className="p-4 rounded-[14px] bg-surface border border-white/[0.06]">
|
|
285
|
+
<div className="text-[14px] text-text-2 font-600 mb-2">{cronHuman}</div>
|
|
286
|
+
{cron && (
|
|
287
|
+
<div className="font-mono text-[12px] text-text-3/50 mb-3">{cron}</div>
|
|
288
|
+
)}
|
|
289
|
+
{nextRuns.length > 0 && (
|
|
290
|
+
<div className="space-y-1.5">
|
|
291
|
+
<div className="text-[11px] text-text-3/60 uppercase tracking-wider font-600">Next runs</div>
|
|
292
|
+
{nextRuns.map((d, i) => (
|
|
293
|
+
<div key={i} className="text-[12px] text-text-3 font-mono">{formatDate(d)}</div>
|
|
294
|
+
))}
|
|
295
|
+
</div>
|
|
296
|
+
)}
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
{scheduleType === 'interval' && (
|
|
302
|
+
<div className="mb-8">
|
|
303
|
+
<SectionLabel>Interval (minutes)</SectionLabel>
|
|
304
|
+
<input
|
|
305
|
+
type="number"
|
|
306
|
+
value={Math.round(intervalMs / 60000)}
|
|
307
|
+
onChange={(e) => setIntervalMs(Math.max(1, parseInt(e.target.value) || 1) * 60000)}
|
|
308
|
+
className={inputClass}
|
|
309
|
+
style={{ fontFamily: 'inherit' }}
|
|
310
|
+
/>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
{editing && (
|
|
315
|
+
<div className="mb-8">
|
|
316
|
+
<SectionLabel>Status</SectionLabel>
|
|
317
|
+
<div className="flex gap-2">
|
|
318
|
+
{(['active', 'paused'] as ScheduleStatus[]).map((s) => (
|
|
319
|
+
<button
|
|
320
|
+
key={s}
|
|
321
|
+
onClick={() => setStatus(s)}
|
|
322
|
+
className={`px-4 py-2 rounded-[10px] text-[13px] font-600 capitalize cursor-pointer transition-all border
|
|
323
|
+
${status === s
|
|
324
|
+
? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
|
|
325
|
+
: 'bg-surface border-white/[0.06] text-text-3'}`}
|
|
326
|
+
style={{ fontFamily: 'inherit' }}
|
|
327
|
+
>
|
|
328
|
+
{s}
|
|
329
|
+
</button>
|
|
330
|
+
))}
|
|
331
|
+
</div>
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
252
334
|
</div>
|
|
253
335
|
)}
|
|
254
336
|
|
|
255
|
-
{
|
|
337
|
+
{/* Step 2: Review */}
|
|
338
|
+
{step === 2 && (
|
|
256
339
|
<div className="mb-8">
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
<
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
>
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
340
|
+
<div className="p-5 rounded-[16px] bg-surface border border-white/[0.06] space-y-4">
|
|
341
|
+
<div>
|
|
342
|
+
<span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Name</span>
|
|
343
|
+
<div className="text-[14px] text-text font-600 mt-0.5">{name}</div>
|
|
344
|
+
</div>
|
|
345
|
+
<div>
|
|
346
|
+
<span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Agent</span>
|
|
347
|
+
<div className="text-[14px] text-text font-600 mt-0.5">{selectedAgent?.name || agentId}</div>
|
|
348
|
+
</div>
|
|
349
|
+
<div>
|
|
350
|
+
<span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Task</span>
|
|
351
|
+
<div className="text-[13px] text-text-2 mt-0.5 whitespace-pre-wrap">{taskPrompt}</div>
|
|
352
|
+
</div>
|
|
353
|
+
<div className="h-px bg-white/[0.06]" />
|
|
354
|
+
<div>
|
|
355
|
+
<span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Schedule</span>
|
|
356
|
+
<div className="text-[14px] text-text font-600 mt-0.5 capitalize">{scheduleType}</div>
|
|
357
|
+
{scheduleType === 'cron' && (
|
|
358
|
+
<div className="text-[12px] text-text-3 font-mono mt-0.5">{cronHuman} ({cron})</div>
|
|
359
|
+
)}
|
|
360
|
+
{scheduleType === 'interval' && (
|
|
361
|
+
<div className="text-[12px] text-text-3 font-mono mt-0.5">Every {Math.round(intervalMs / 60000)} minutes</div>
|
|
362
|
+
)}
|
|
363
|
+
{scheduleType === 'once' && (
|
|
364
|
+
<div className="text-[12px] text-text-3 font-mono mt-0.5">Run once</div>
|
|
365
|
+
)}
|
|
366
|
+
</div>
|
|
367
|
+
{editing && (
|
|
368
|
+
<div>
|
|
369
|
+
<span className="text-[11px] text-text-3/50 uppercase tracking-wider font-600">Status</span>
|
|
370
|
+
<div className="text-[14px] text-text font-600 mt-0.5 capitalize">{status}</div>
|
|
371
|
+
</div>
|
|
372
|
+
)}
|
|
272
373
|
</div>
|
|
273
374
|
</div>
|
|
274
375
|
)}
|
|
275
376
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
saveLabel={editing ? 'Save' : 'Create'}
|
|
280
|
-
saveDisabled={!name.trim() || !agentId}
|
|
281
|
-
left={editing && (
|
|
377
|
+
{/* Footer */}
|
|
378
|
+
<div className="flex gap-3 pt-2 border-t border-white/[0.04]">
|
|
379
|
+
{editing && step === 0 && (
|
|
282
380
|
<button onClick={handleDelete} className="py-3.5 px-6 rounded-[14px] border border-red-500/20 bg-transparent text-red-400 text-[15px] font-600 cursor-pointer hover:bg-red-500/10 transition-all" style={{ fontFamily: 'inherit' }}>
|
|
283
381
|
Delete
|
|
284
382
|
</button>
|
|
285
383
|
)}
|
|
286
|
-
|
|
384
|
+
{step > 0 && (
|
|
385
|
+
<button
|
|
386
|
+
onClick={() => setStep((step - 1) as Step)}
|
|
387
|
+
className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
|
|
388
|
+
style={{ fontFamily: 'inherit' }}
|
|
389
|
+
>
|
|
390
|
+
Back
|
|
391
|
+
</button>
|
|
392
|
+
)}
|
|
393
|
+
<div className="flex-1" />
|
|
394
|
+
<button
|
|
395
|
+
onClick={onClose}
|
|
396
|
+
className="py-3.5 px-6 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
|
|
397
|
+
style={{ fontFamily: 'inherit' }}
|
|
398
|
+
>
|
|
399
|
+
Cancel
|
|
400
|
+
</button>
|
|
401
|
+
{step < 2 ? (
|
|
402
|
+
<button
|
|
403
|
+
onClick={() => setStep((step + 1) as Step)}
|
|
404
|
+
disabled={step === 0 ? !step0Valid : !step1Valid}
|
|
405
|
+
className="py-3.5 px-8 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
|
|
406
|
+
style={{ fontFamily: 'inherit' }}
|
|
407
|
+
>
|
|
408
|
+
Next
|
|
409
|
+
</button>
|
|
410
|
+
) : (
|
|
411
|
+
<button
|
|
412
|
+
onClick={handleSave}
|
|
413
|
+
className="py-3.5 px-8 rounded-[14px] border-none bg-accent-bright text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110"
|
|
414
|
+
style={{ fontFamily: 'inherit' }}
|
|
415
|
+
>
|
|
416
|
+
{editing ? 'Save' : 'Create'}
|
|
417
|
+
</button>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
287
420
|
</BottomSheet>
|
|
288
421
|
)
|
|
289
422
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useSyncExternalStore } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
+
|
|
7
|
+
// Module-level gateway status store with subscribe/getSnapshot for useSyncExternalStore
|
|
8
|
+
let _status: 'connected' | 'disconnected' | null = null
|
|
9
|
+
let _lastCheck = 0
|
|
10
|
+
const _listeners = new Set<() => void>()
|
|
11
|
+
const POLL_INTERVAL = 30_000
|
|
12
|
+
|
|
13
|
+
function getSnapshot() {
|
|
14
|
+
return _status
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function subscribe(cb: () => void) {
|
|
18
|
+
_listeners.add(cb)
|
|
19
|
+
return () => { _listeners.delete(cb) }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function checkGateway() {
|
|
23
|
+
try {
|
|
24
|
+
const res = await api<{ connected: boolean }>('GET', '/openclaw/gateway')
|
|
25
|
+
_status = res.connected ? 'connected' : 'disconnected'
|
|
26
|
+
} catch {
|
|
27
|
+
_status = 'disconnected'
|
|
28
|
+
}
|
|
29
|
+
_lastCheck = Date.now()
|
|
30
|
+
for (const cb of _listeners) cb()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function useGatewayStatus() {
|
|
34
|
+
const status = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
35
|
+
const startedRef = useRef(false)
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (startedRef.current) return
|
|
39
|
+
startedRef.current = true
|
|
40
|
+
// Initial check if stale
|
|
41
|
+
if (!_status || Date.now() - _lastCheck >= POLL_INTERVAL) {
|
|
42
|
+
checkGateway()
|
|
43
|
+
}
|
|
44
|
+
const interval = setInterval(checkGateway, POLL_INTERVAL)
|
|
45
|
+
return () => clearInterval(interval)
|
|
46
|
+
}, [])
|
|
47
|
+
|
|
48
|
+
return status
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function GatewayDisconnectOverlay() {
|
|
52
|
+
const setActiveView = useAppStore((s) => s.setActiveView)
|
|
53
|
+
const setSidebarOpen = useAppStore((s) => s.setSidebarOpen)
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="absolute inset-0 z-10 flex items-center justify-center bg-bg/60 backdrop-blur-sm">
|
|
57
|
+
<div className="flex flex-col items-center gap-4 p-8 rounded-[20px] border border-white/[0.06] bg-surface/90 max-w-[320px] text-center">
|
|
58
|
+
<span className="inline-flex items-center justify-center w-10 h-10 rounded-full bg-red-500/10">
|
|
59
|
+
<span className="w-3 h-3 rounded-full bg-red-400" />
|
|
60
|
+
</span>
|
|
61
|
+
<div>
|
|
62
|
+
<h3 className="font-display text-[16px] font-600 text-text mb-1">Gateway Disconnected</h3>
|
|
63
|
+
<p className="text-[13px] text-text-3/60">
|
|
64
|
+
The OpenClaw gateway is offline. Connect to resume chatting with this agent.
|
|
65
|
+
</p>
|
|
66
|
+
</div>
|
|
67
|
+
<button
|
|
68
|
+
onClick={() => {
|
|
69
|
+
setActiveView('settings')
|
|
70
|
+
setSidebarOpen(true)
|
|
71
|
+
}}
|
|
72
|
+
className="px-5 py-2 rounded-[10px] border-none bg-accent-bright text-white text-[13px] font-600 cursor-pointer transition-all hover:brightness-110"
|
|
73
|
+
style={{ fontFamily: 'inherit' }}
|
|
74
|
+
>
|
|
75
|
+
Connect Gateway
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
@@ -76,7 +76,7 @@ export function AgentSwitchDialog() {
|
|
|
76
76
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
77
77
|
<DialogContent
|
|
78
78
|
showCloseButton={false}
|
|
79
|
-
className="sm:max-w-[440px] p-0 bg-
|
|
79
|
+
className="sm:max-w-[440px] p-0 bg-surface/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
|
|
80
80
|
onKeyDown={handleKeyDown}
|
|
81
81
|
>
|
|
82
82
|
<DialogTitle className="sr-only">Switch Agent</DialogTitle>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { CheckIcon } from '@/components/shared/check-icon'
|
|
4
|
+
import type { Chatroom } from '@/types'
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
chatrooms: Chatroom[]
|
|
8
|
+
selected: string
|
|
9
|
+
onSelect: (chatroomId: string) => void
|
|
10
|
+
maxHeight?: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ChatroomPickerList({
|
|
14
|
+
chatrooms,
|
|
15
|
+
selected,
|
|
16
|
+
onSelect,
|
|
17
|
+
maxHeight = 220,
|
|
18
|
+
}: Props) {
|
|
19
|
+
if (chatrooms.length === 0) {
|
|
20
|
+
return <p className="text-[13px] text-text-3">No chat rooms created yet.</p>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className="flex flex-col gap-1 rounded-[14px] border border-white/[0.06] bg-surface p-1.5 overflow-y-auto"
|
|
26
|
+
style={{ maxHeight }}
|
|
27
|
+
>
|
|
28
|
+
{chatrooms.map((cr) => {
|
|
29
|
+
const active = selected === cr.id
|
|
30
|
+
return (
|
|
31
|
+
<button
|
|
32
|
+
key={cr.id}
|
|
33
|
+
onClick={() => onSelect(cr.id)}
|
|
34
|
+
className={`relative flex items-center gap-3 px-3 py-2.5 rounded-[10px] cursor-pointer transition-all w-full text-left border-none
|
|
35
|
+
${active ? 'bg-accent-soft' : 'bg-transparent hover:bg-white/[0.03]'}`}
|
|
36
|
+
style={{ fontFamily: 'inherit' }}
|
|
37
|
+
>
|
|
38
|
+
{active && (
|
|
39
|
+
<div className="absolute left-0 top-2 bottom-2 w-[2.5px] rounded-full bg-accent-bright" />
|
|
40
|
+
)}
|
|
41
|
+
<div className="w-[28px] h-[28px] rounded-full bg-white/[0.06] flex items-center justify-center shrink-0">
|
|
42
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={active ? 'text-accent-bright' : 'text-text-3'}>
|
|
43
|
+
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
|
|
44
|
+
</svg>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="flex-1 min-w-0">
|
|
47
|
+
<span className={`text-[13px] font-600 block truncate ${active ? 'text-accent-bright' : 'text-text-2'}`}>
|
|
48
|
+
{cr.name}
|
|
49
|
+
</span>
|
|
50
|
+
<span className="text-[11px] text-text-3/60 block truncate">
|
|
51
|
+
{cr.agentIds.length} agent{cr.agentIds.length !== 1 ? 's' : ''}
|
|
52
|
+
{cr.chatMode === 'parallel' ? ' · parallel' : ' · sequential'}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
{active && <CheckIcon className="text-accent-bright shrink-0" />}
|
|
56
|
+
</button>
|
|
57
|
+
)
|
|
58
|
+
})}
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|