@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.
Files changed (109) hide show
  1. package/README.md +15 -2
  2. package/bin/server-cmd.js +1 -0
  3. package/package.json +2 -1
  4. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +10 -136
  6. package/src/app/api/connectors/[id]/route.ts +1 -0
  7. package/src/app/api/connectors/route.ts +2 -1
  8. package/src/app/api/files/open/route.ts +43 -0
  9. package/src/app/api/search/route.ts +9 -7
  10. package/src/app/api/sessions/[id]/messages/route.ts +70 -2
  11. package/src/app/api/sessions/[id]/route.ts +4 -0
  12. package/src/app/api/tasks/metrics/route.ts +101 -0
  13. package/src/app/api/tasks/route.ts +17 -2
  14. package/src/app/api/tts/route.ts +3 -2
  15. package/src/app/api/tts/stream/route.ts +3 -2
  16. package/src/app/api/uploads/[filename]/route.ts +19 -34
  17. package/src/app/api/uploads/route.ts +94 -0
  18. package/src/app/globals.css +5 -0
  19. package/src/cli/index.js +16 -1
  20. package/src/cli/spec.js +26 -0
  21. package/src/components/agents/agent-card.tsx +3 -3
  22. package/src/components/agents/agent-chat-list.tsx +29 -6
  23. package/src/components/agents/agent-sheet.tsx +66 -4
  24. package/src/components/agents/inspector-panel.tsx +81 -6
  25. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  26. package/src/components/agents/personality-builder.tsx +42 -14
  27. package/src/components/agents/soul-library-picker.tsx +89 -0
  28. package/src/components/canvas/canvas-panel.tsx +96 -0
  29. package/src/components/chat/activity-moment.tsx +8 -4
  30. package/src/components/chat/chat-area.tsx +46 -22
  31. package/src/components/chat/chat-header.tsx +455 -286
  32. package/src/components/chat/chat-preview-panel.tsx +1 -2
  33. package/src/components/chat/delegation-banner.tsx +371 -0
  34. package/src/components/chat/file-path-chip.tsx +23 -2
  35. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  36. package/src/components/chat/message-bubble.tsx +315 -25
  37. package/src/components/chat/message-list.tsx +180 -7
  38. package/src/components/chat/streaming-bubble.tsx +68 -1
  39. package/src/components/chat/tool-call-bubble.tsx +45 -3
  40. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  41. package/src/components/chatrooms/chatroom-list.tsx +8 -1
  42. package/src/components/chatrooms/chatroom-message.tsx +8 -3
  43. package/src/components/chatrooms/chatroom-view.tsx +3 -3
  44. package/src/components/connectors/connector-list.tsx +168 -90
  45. package/src/components/connectors/connector-sheet.tsx +68 -16
  46. package/src/components/home/home-view.tsx +1 -1
  47. package/src/components/input/chat-input.tsx +28 -2
  48. package/src/components/layout/app-layout.tsx +19 -2
  49. package/src/components/projects/project-detail.tsx +1 -1
  50. package/src/components/schedules/schedule-sheet.tsx +260 -127
  51. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  52. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  53. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  54. package/src/components/shared/connector-platform-icon.tsx +51 -4
  55. package/src/components/shared/icon-button.tsx +16 -2
  56. package/src/components/shared/keyboard-shortcuts-dialog.tsx +1 -1
  57. package/src/components/shared/search-dialog.tsx +17 -10
  58. package/src/components/shared/settings/section-embedding.tsx +48 -13
  59. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  60. package/src/components/shared/settings/section-storage.tsx +206 -0
  61. package/src/components/shared/settings/section-user-preferences.tsx +18 -0
  62. package/src/components/shared/settings/section-voice.tsx +42 -21
  63. package/src/components/shared/settings/section-web-search.tsx +30 -6
  64. package/src/components/shared/settings/settings-page.tsx +3 -1
  65. package/src/components/shared/settings/storage-browser.tsx +259 -0
  66. package/src/components/tasks/task-card.tsx +14 -1
  67. package/src/components/tasks/task-sheet.tsx +328 -3
  68. package/src/components/usage/metrics-dashboard.tsx +90 -6
  69. package/src/hooks/use-continuous-speech.ts +10 -4
  70. package/src/hooks/use-voice-conversation.ts +53 -10
  71. package/src/hooks/use-ws.ts +4 -2
  72. package/src/lib/providers/anthropic.ts +13 -7
  73. package/src/lib/providers/index.ts +1 -0
  74. package/src/lib/providers/openai.ts +13 -7
  75. package/src/lib/server/chat-execution.ts +51 -11
  76. package/src/lib/server/chatroom-helpers.ts +146 -0
  77. package/src/lib/server/connectors/manager.ts +218 -7
  78. package/src/lib/server/heartbeat-service.ts +8 -1
  79. package/src/lib/server/main-agent-loop.ts +1 -1
  80. package/src/lib/server/memory-consolidation.ts +15 -2
  81. package/src/lib/server/memory-db.ts +134 -6
  82. package/src/lib/server/mime.ts +51 -0
  83. package/src/lib/server/openclaw-gateway.ts +2 -2
  84. package/src/lib/server/orchestrator-lg.ts +2 -0
  85. package/src/lib/server/orchestrator.ts +5 -2
  86. package/src/lib/server/playwright-proxy.mjs +2 -3
  87. package/src/lib/server/prompt-runtime-context.ts +53 -0
  88. package/src/lib/server/queue.ts +52 -7
  89. package/src/lib/server/session-tools/canvas.ts +67 -0
  90. package/src/lib/server/session-tools/connector.ts +83 -9
  91. package/src/lib/server/session-tools/crud.ts +21 -0
  92. package/src/lib/server/session-tools/delegate.ts +68 -4
  93. package/src/lib/server/session-tools/git.ts +71 -0
  94. package/src/lib/server/session-tools/http.ts +57 -0
  95. package/src/lib/server/session-tools/index.ts +8 -0
  96. package/src/lib/server/session-tools/memory.ts +1 -0
  97. package/src/lib/server/session-tools/search-providers.ts +16 -8
  98. package/src/lib/server/session-tools/subagent.ts +106 -0
  99. package/src/lib/server/session-tools/web.ts +115 -4
  100. package/src/lib/server/stream-agent-chat.ts +32 -10
  101. package/src/lib/server/task-mention.ts +41 -0
  102. package/src/lib/sessions.ts +10 -0
  103. package/src/lib/soul-library.ts +103 -0
  104. package/src/lib/task-dedupe.ts +26 -0
  105. package/src/lib/tool-definitions.ts +2 -0
  106. package/src/lib/tts.ts +2 -2
  107. package/src/stores/use-app-store.ts +5 -1
  108. package/src/stores/use-chat-store.ts +65 -2
  109. 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 getNextRuns(cron: string, count: number = 3): Date[] {
20
+ async function getNextRunsAsync(cron: string, count: number = 3): Promise<Date[]> {
22
21
  try {
23
- // Simple cron parser for next N runs
24
- const { parseExpression } = require('cron-parser')
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 = useMemo(() => getNextRuns(cron), [cron])
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-10">
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
- <div className="mb-8">
142
- <SectionLabel>Name</SectionLabel>
143
- <input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Daily keyword research" className={inputClass} style={{ fontFamily: 'inherit' }} />
144
- </div>
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
- key={t}
174
- onClick={() => setScheduleType(t)}
175
- className={`py-3.5 px-4 rounded-[14px] text-center cursor-pointer transition-all duration-200
176
- active:scale-[0.97] text-[14px] font-600 capitalize border
177
- ${scheduleType === t
178
- ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
179
- : 'bg-surface border-white/[0.06] text-text-2 hover:bg-surface-2'}`}
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
- {t}
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
- </div>
188
+ </div>
189
+ ))}
186
190
  </div>
187
191
 
188
- {scheduleType === 'cron' && (
189
- <div className="mb-8">
190
- <SectionLabel>Schedule</SectionLabel>
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
- {/* Preset buttons */}
193
- <div className="flex flex-wrap gap-2 mb-4">
194
- {CRON_PRESETS.map((p) => (
195
- <button
196
- key={p.cron}
197
- onClick={() => { setCron(p.cron); setCustomCron(false) }}
198
- className={`px-3.5 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
199
- ${cron === p.cron && !customCron
200
- ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
201
- : 'bg-surface border-white/[0.06] text-text-3 hover:text-text-2'}`}
202
- style={{ fontFamily: 'inherit' }}
203
- >
204
- {p.label}
205
- </button>
206
- ))}
207
- <button
208
- onClick={() => setCustomCron(true)}
209
- className={`px-3.5 py-2 rounded-[10px] text-[13px] font-600 cursor-pointer transition-all border
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
- {/* Custom cron input */}
220
- {customCron && (
221
- <input type="text" value={cron} onChange={(e) => setCron(e.target.value)} placeholder="0 * * * *" className={`${inputClass} font-mono text-[14px] mb-3`} />
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
- {/* Human-readable preview */}
225
- <div className="p-4 rounded-[14px] bg-surface border border-white/[0.06]">
226
- <div className="text-[14px] text-text-2 font-600 mb-2">{cronHuman}</div>
227
- {cron && (
228
- <div className="font-mono text-[12px] text-text-3/50 mb-3">{cron}</div>
229
- )}
230
- {nextRuns.length > 0 && (
231
- <div className="space-y-1.5">
232
- <div className="text-[11px] text-text-3/60 uppercase tracking-wider font-600">Next runs</div>
233
- {nextRuns.map((d, i) => (
234
- <div key={i} className="text-[12px] text-text-3 font-mono">{formatDate(d)}</div>
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
- {scheduleType === 'interval' && (
243
- <div className="mb-8">
244
- <SectionLabel>Interval (minutes)</SectionLabel>
245
- <input
246
- type="number"
247
- value={Math.round(intervalMs / 60000)}
248
- onChange={(e) => setIntervalMs(Math.max(1, parseInt(e.target.value) || 1) * 60000)}
249
- className={inputClass}
250
- style={{ fontFamily: 'inherit' }}
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
- {editing && (
337
+ {/* Step 2: Review */}
338
+ {step === 2 && (
256
339
  <div className="mb-8">
257
- <SectionLabel>Status</SectionLabel>
258
- <div className="flex gap-2">
259
- {(['active', 'paused'] as ScheduleStatus[]).map((s) => (
260
- <button
261
- key={s}
262
- onClick={() => setStatus(s)}
263
- className={`px-4 py-2 rounded-[10px] text-[13px] font-600 capitalize cursor-pointer transition-all border
264
- ${status === s
265
- ? 'bg-accent-soft border-accent-bright/25 text-accent-bright'
266
- : 'bg-surface border-white/[0.06] text-text-3'}`}
267
- style={{ fontFamily: 'inherit' }}
268
- >
269
- {s}
270
- </button>
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
- <SheetFooter
277
- onCancel={onClose}
278
- onSave={handleSave}
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-[#1a1a2e]/95 backdrop-blur-xl border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] rounded-[16px] overflow-hidden gap-0"
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
+ }