@swarmclawai/swarmclaw 0.6.4 → 0.6.7

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 (143) hide show
  1. package/README.md +62 -30
  2. package/package.json +10 -1
  3. package/src/app/api/agents/[id]/clone/route.ts +40 -0
  4. package/src/app/api/agents/route.ts +39 -14
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +34 -2
  8. package/src/app/api/chatrooms/route.ts +26 -3
  9. package/src/app/api/connectors/[id]/health/route.ts +64 -0
  10. package/src/app/api/connectors/route.ts +17 -2
  11. package/src/app/api/knowledge/route.ts +6 -1
  12. package/src/app/api/openclaw/doctor/route.ts +17 -0
  13. package/src/app/api/schedules/[id]/run/route.ts +3 -0
  14. package/src/app/api/sessions/[id]/chat/route.ts +5 -1
  15. package/src/app/api/sessions/route.ts +11 -2
  16. package/src/app/api/tasks/[id]/route.ts +18 -13
  17. package/src/app/api/tasks/route.ts +44 -1
  18. package/src/app/api/usage/route.ts +16 -7
  19. package/src/app/api/wallets/[id]/approve/route.ts +62 -0
  20. package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
  21. package/src/app/api/wallets/[id]/route.ts +118 -0
  22. package/src/app/api/wallets/[id]/send/route.ts +118 -0
  23. package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
  24. package/src/app/api/wallets/route.ts +74 -0
  25. package/src/app/globals.css +8 -0
  26. package/src/cli/index.js +20 -0
  27. package/src/cli/index.ts +223 -39
  28. package/src/cli/spec.js +14 -0
  29. package/src/components/agents/agent-avatar.tsx +15 -1
  30. package/src/components/agents/agent-card.tsx +38 -6
  31. package/src/components/agents/agent-chat-list.tsx +79 -3
  32. package/src/components/agents/agent-sheet.tsx +191 -26
  33. package/src/components/auth/setup-wizard.tsx +268 -353
  34. package/src/components/chat/chat-area.tsx +24 -9
  35. package/src/components/chat/chat-header.tsx +48 -19
  36. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  37. package/src/components/chat/delegation-banner.test.ts +27 -0
  38. package/src/components/chat/delegation-banner.tsx +109 -23
  39. package/src/components/chat/message-bubble.tsx +17 -16
  40. package/src/components/chat/message-list.tsx +6 -5
  41. package/src/components/chat/streaming-bubble.tsx +3 -2
  42. package/src/components/chat/thinking-indicator.tsx +3 -2
  43. package/src/components/chat/transfer-agent-picker.tsx +1 -1
  44. package/src/components/chatrooms/agent-hover-card.tsx +1 -1
  45. package/src/components/chatrooms/chatroom-input.tsx +1 -1
  46. package/src/components/chatrooms/chatroom-message.tsx +165 -23
  47. package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
  48. package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
  49. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  50. package/src/components/connectors/connector-health.tsx +120 -0
  51. package/src/components/connectors/connector-list.tsx +1 -1
  52. package/src/components/connectors/connector-sheet.tsx +9 -0
  53. package/src/components/home/home-view.tsx +25 -3
  54. package/src/components/input/chat-input.tsx +8 -1
  55. package/src/components/knowledge/knowledge-list.tsx +1 -1
  56. package/src/components/knowledge/knowledge-sheet.tsx +1 -1
  57. package/src/components/layout/app-layout.tsx +35 -4
  58. package/src/components/memory/memory-agent-list.tsx +1 -1
  59. package/src/components/memory/memory-browser.tsx +1 -0
  60. package/src/components/memory/memory-card.tsx +3 -2
  61. package/src/components/memory/memory-detail.tsx +3 -3
  62. package/src/components/memory/memory-sheet.tsx +2 -2
  63. package/src/components/projects/project-detail.tsx +4 -4
  64. package/src/components/schedules/schedule-list.tsx +55 -9
  65. package/src/components/schedules/schedule-sheet.tsx +134 -23
  66. package/src/components/secrets/secret-sheet.tsx +1 -1
  67. package/src/components/secrets/secrets-list.tsx +1 -1
  68. package/src/components/sessions/session-card.tsx +1 -1
  69. package/src/components/shared/agent-picker-list.tsx +1 -1
  70. package/src/components/shared/agent-switch-dialog.tsx +1 -1
  71. package/src/components/shared/command-palette.tsx +237 -0
  72. package/src/components/shared/connector-platform-icon.tsx +1 -0
  73. package/src/components/shared/settings/section-user-preferences.tsx +4 -4
  74. package/src/components/skills/skill-list.tsx +1 -1
  75. package/src/components/skills/skill-sheet.tsx +1 -1
  76. package/src/components/tasks/task-board.tsx +3 -3
  77. package/src/components/tasks/task-card.tsx +22 -2
  78. package/src/components/tasks/task-sheet.tsx +112 -17
  79. package/src/components/usage/metrics-dashboard.tsx +13 -25
  80. package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
  81. package/src/components/wallets/wallet-panel.tsx +616 -0
  82. package/src/components/wallets/wallet-section.tsx +100 -0
  83. package/src/hooks/use-swipe.ts +49 -0
  84. package/src/lib/providers/anthropic.ts +16 -2
  85. package/src/lib/providers/claude-cli.ts +7 -1
  86. package/src/lib/providers/index.ts +7 -0
  87. package/src/lib/providers/ollama.ts +16 -2
  88. package/src/lib/providers/openai.ts +7 -2
  89. package/src/lib/providers/openclaw.ts +6 -1
  90. package/src/lib/providers/provider-defaults.ts +7 -0
  91. package/src/lib/schedule-templates.ts +115 -0
  92. package/src/lib/server/agent-registry.ts +2 -2
  93. package/src/lib/server/alert-dispatch.ts +64 -0
  94. package/src/lib/server/chat-execution.ts +76 -4
  95. package/src/lib/server/chatroom-health.ts +60 -0
  96. package/src/lib/server/chatroom-helpers.test.ts +94 -0
  97. package/src/lib/server/chatroom-helpers.ts +86 -12
  98. package/src/lib/server/chatroom-routing.ts +65 -0
  99. package/src/lib/server/connectors/discord.ts +3 -0
  100. package/src/lib/server/connectors/email.ts +267 -0
  101. package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
  102. package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
  103. package/src/lib/server/connectors/manager.ts +239 -5
  104. package/src/lib/server/connectors/openclaw.ts +3 -0
  105. package/src/lib/server/connectors/slack.ts +6 -0
  106. package/src/lib/server/connectors/telegram.ts +18 -0
  107. package/src/lib/server/connectors/types.ts +2 -0
  108. package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
  109. package/src/lib/server/connectors/whatsapp-text.ts +26 -0
  110. package/src/lib/server/connectors/whatsapp.ts +17 -5
  111. package/src/lib/server/cost.ts +70 -0
  112. package/src/lib/server/create-notification.ts +2 -0
  113. package/src/lib/server/daemon-state.ts +124 -0
  114. package/src/lib/server/dag-validation.ts +115 -0
  115. package/src/lib/server/memory-db.ts +12 -7
  116. package/src/lib/server/openclaw-doctor.ts +48 -0
  117. package/src/lib/server/orchestrator-lg.ts +12 -2
  118. package/src/lib/server/orchestrator.ts +6 -1
  119. package/src/lib/server/queue-followups.test.ts +224 -0
  120. package/src/lib/server/queue.ts +238 -24
  121. package/src/lib/server/scheduler.ts +3 -0
  122. package/src/lib/server/session-run-manager.ts +22 -1
  123. package/src/lib/server/session-tools/chatroom.ts +11 -2
  124. package/src/lib/server/session-tools/context-mgmt.ts +2 -2
  125. package/src/lib/server/session-tools/index.ts +8 -2
  126. package/src/lib/server/session-tools/memory.ts +23 -4
  127. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  128. package/src/lib/server/session-tools/shell.ts +1 -1
  129. package/src/lib/server/session-tools/wallet.ts +124 -0
  130. package/src/lib/server/session-tools/web.ts +2 -2
  131. package/src/lib/server/solana.ts +122 -0
  132. package/src/lib/server/storage.ts +158 -6
  133. package/src/lib/server/stream-agent-chat.ts +126 -63
  134. package/src/lib/server/task-mention.test.ts +41 -0
  135. package/src/lib/server/task-mention.ts +3 -2
  136. package/src/lib/setup-defaults.ts +277 -0
  137. package/src/lib/tool-definitions.ts +1 -0
  138. package/src/lib/validation/schemas.ts +69 -0
  139. package/src/lib/view-routes.ts +1 -0
  140. package/src/stores/use-app-store.ts +15 -3
  141. package/src/stores/use-chatroom-store.ts +52 -2
  142. package/src/types/index.ts +98 -2
  143. package/tsconfig.json +2 -1
@@ -68,7 +68,7 @@ function AssignAgentPicker({ projectId, onClose }: { projectId: string; onClose:
68
68
  className="w-full flex items-center gap-2.5 px-3 py-2 rounded-[8px] text-left hover:bg-white/[0.06] transition-colors cursor-pointer bg-transparent border-none"
69
69
  style={{ fontFamily: 'inherit' }}
70
70
  >
71
- <AgentAvatar seed={a.avatarSeed} name={a.name} size={22} />
71
+ <AgentAvatar seed={a.avatarSeed} avatarUrl={a.avatarUrl} name={a.name} size={22} />
72
72
  <div className="min-w-0 flex-1">
73
73
  <div className="text-[12px] text-text truncate">{a.name}</div>
74
74
  <div className="text-[10px] text-text-3/40 truncate">{a.model || a.provider}</div>
@@ -312,7 +312,7 @@ export function ProjectDetail() {
312
312
  className="flex items-center gap-3 flex-1 min-w-0 cursor-pointer bg-transparent border-none text-left p-0"
313
313
  style={{ fontFamily: 'inherit' }}
314
314
  >
315
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={28} />
315
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={28} />
316
316
  <div className="min-w-0 flex-1">
317
317
  <div className="text-[13px] font-600 text-text truncate">{agent.name}</div>
318
318
  <div className="text-[11px] text-text-3/50 truncate">{agent.model || agent.provider}</div>
@@ -381,7 +381,7 @@ export function ProjectDetail() {
381
381
  <span className="text-[13px] text-text truncate flex-1">{task.title}</span>
382
382
  {agent && (
383
383
  <span className="shrink-0 flex items-center gap-1.5 text-[11px] text-text-3/40">
384
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={16} />
384
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={16} />
385
385
  {agent.name}
386
386
  </span>
387
387
  )}
@@ -448,7 +448,7 @@ export function ProjectDetail() {
448
448
  </span>
449
449
  {agent && (
450
450
  <span className="shrink-0 flex items-center gap-1.5 text-[11px] text-text-3/40">
451
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={16} />
451
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={16} />
452
452
  </span>
453
453
  )}
454
454
  {schedule.nextRunAt && (
@@ -3,6 +3,16 @@
3
3
  import { useEffect, useMemo, useState } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { ScheduleCard } from './schedule-card'
6
+ import { SCHEDULE_TEMPLATES, FEATURED_TEMPLATE_IDS } from '@/lib/schedule-templates'
7
+ import { Newspaper, HeartPulse, PenLine, FileText } from 'lucide-react'
8
+
9
+ const FEATURED_ICONS: Record<string, React.ComponentType<{ className?: string; size?: number }>> = {
10
+ Newspaper, HeartPulse, PenLine, FileText,
11
+ }
12
+
13
+ const featuredTemplates = SCHEDULE_TEMPLATES.filter((t) =>
14
+ (FEATURED_TEMPLATE_IDS as readonly string[]).includes(t.id),
15
+ )
6
16
 
7
17
  interface Props {
8
18
  inSidebar?: boolean
@@ -12,6 +22,7 @@ export function ScheduleList({ inSidebar }: Props) {
12
22
  const schedules = useAppStore((s) => s.schedules)
13
23
  const loadSchedules = useAppStore((s) => s.loadSchedules)
14
24
  const setScheduleSheetOpen = useAppStore((s) => s.setScheduleSheetOpen)
25
+ const setTemplatePrefill = useAppStore((s) => s.setScheduleTemplatePrefill)
15
26
  const activeProjectFilter = useAppStore((s) => s.activeProjectFilter)
16
27
  const [search, setSearch] = useState('')
17
28
 
@@ -39,15 +50,50 @@ export function ScheduleList({ inSidebar }: Props) {
39
50
  <p className="font-display text-[15px] font-600 text-text-2">No schedules yet</p>
40
51
  <p className="text-[13px] text-text-3/50">Automate tasks with cron or intervals</p>
41
52
  {!inSidebar && (
42
- <button
43
- onClick={() => setScheduleSheetOpen(true)}
44
- className="mt-3 px-8 py-3 rounded-[14px] border-none bg-accent-bright text-white
45
- text-[14px] font-600 cursor-pointer active:scale-95 transition-all duration-200
46
- shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
47
- style={{ fontFamily: 'inherit' }}
48
- >
49
- + New Schedule
50
- </button>
53
+ <>
54
+ <button
55
+ onClick={() => setScheduleSheetOpen(true)}
56
+ className="mt-3 px-8 py-3 rounded-[14px] border-none bg-accent-bright text-white
57
+ text-[14px] font-600 cursor-pointer active:scale-95 transition-all duration-200
58
+ shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
59
+ style={{ fontFamily: 'inherit' }}
60
+ >
61
+ + New Schedule
62
+ </button>
63
+ <div className="mt-6 w-full max-w-lg">
64
+ <p className="text-[12px] text-text-3/40 uppercase tracking-wider font-600 mb-3">Quick start</p>
65
+ <div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
66
+ {featuredTemplates.map((tpl) => {
67
+ const IconComp = FEATURED_ICONS[tpl.icon] || FileText
68
+ return (
69
+ <button
70
+ key={tpl.id}
71
+ onClick={() => {
72
+ setTemplatePrefill({
73
+ name: tpl.name,
74
+ taskPrompt: tpl.defaults.taskPrompt,
75
+ scheduleType: tpl.defaults.scheduleType,
76
+ cron: tpl.defaults.cron,
77
+ intervalMs: tpl.defaults.intervalMs,
78
+ })
79
+ setScheduleSheetOpen(true)
80
+ }}
81
+ className="flex flex-col items-center gap-2 p-4 rounded-[14px] border border-white/[0.06]
82
+ bg-surface cursor-pointer transition-all duration-200 hover:bg-surface-2
83
+ hover:border-white/[0.1] active:scale-[0.97]"
84
+ style={{ fontFamily: 'inherit' }}
85
+ >
86
+ <div className="w-8 h-8 rounded-[8px] bg-accent-soft flex items-center justify-center">
87
+ <IconComp size={14} className="text-accent-bright" />
88
+ </div>
89
+ <span className="text-[12px] font-600 text-text-2">{tpl.name}</span>
90
+ <span className="text-[11px] text-text-3/50 leading-[1.3]">{tpl.description}</span>
91
+ </button>
92
+ )
93
+ })}
94
+ </div>
95
+ </div>
96
+ </>
51
97
  )}
52
98
  </div>
53
99
  )
@@ -9,6 +9,16 @@ import { inputClass } from '@/components/shared/form-styles'
9
9
  import type { ScheduleType, ScheduleStatus } from '@/types'
10
10
  import cronstrue from 'cronstrue'
11
11
  import { SectionLabel } from '@/components/shared/section-label'
12
+ import { SCHEDULE_TEMPLATES, type ScheduleTemplate } from '@/lib/schedule-templates'
13
+ import {
14
+ Newspaper, BarChart3, HeartPulse, PenLine, Trash2,
15
+ Activity, ShieldCheck, DatabaseBackup, FileText,
16
+ } from 'lucide-react'
17
+
18
+ const TEMPLATE_ICONS: Record<string, React.ComponentType<{ className?: string; size?: number }>> = {
19
+ Newspaper, BarChart3, HeartPulse, PenLine, Trash2,
20
+ Activity, ShieldCheck, DatabaseBackup, FileText,
21
+ }
12
22
 
13
23
  const CRON_PRESETS = [
14
24
  { label: 'Every hour', cron: '0 * * * *' },
@@ -44,8 +54,30 @@ function formatDate(d: Date): string {
44
54
  ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
45
55
  }
46
56
 
47
- const STEPS = ['What', 'When', 'Review'] as const
48
- type Step = 0 | 1 | 2
57
+ const STEPS_CREATE = ['Template', 'What', 'When', 'Review'] as const
58
+ const STEPS_EDIT = ['What', 'When', 'Review'] as const
59
+ type Step = 0 | 1 | 2 | 3
60
+
61
+ function applyTemplate(
62
+ tpl: ScheduleTemplate,
63
+ setters: {
64
+ setName: (v: string) => void
65
+ setTaskPrompt: (v: string) => void
66
+ setScheduleType: (v: ScheduleType) => void
67
+ setCron: (v: string) => void
68
+ setIntervalMs: (v: number) => void
69
+ setCustomCron: (v: boolean) => void
70
+ },
71
+ ) {
72
+ setters.setName(tpl.name)
73
+ setters.setTaskPrompt(tpl.defaults.taskPrompt)
74
+ setters.setScheduleType(tpl.defaults.scheduleType)
75
+ if (tpl.defaults.cron) {
76
+ setters.setCron(tpl.defaults.cron)
77
+ setters.setCustomCron(!CRON_PRESETS.some((p) => p.cron === tpl.defaults.cron))
78
+ }
79
+ if (tpl.defaults.intervalMs) setters.setIntervalMs(tpl.defaults.intervalMs)
80
+ }
49
81
 
50
82
  export function ScheduleSheet() {
51
83
  const open = useAppStore((s) => s.scheduleSheetOpen)
@@ -56,6 +88,8 @@ export function ScheduleSheet() {
56
88
  const loadSchedules = useAppStore((s) => s.loadSchedules)
57
89
  const agents = useAppStore((s) => s.agents)
58
90
  const loadAgents = useAppStore((s) => s.loadAgents)
91
+ const templatePrefill = useAppStore((s) => s.scheduleTemplatePrefill)
92
+ const setTemplatePrefill = useAppStore((s) => s.setScheduleTemplatePrefill)
59
93
 
60
94
  const [step, setStep] = useState<Step>(0)
61
95
  const [name, setName] = useState('')
@@ -68,13 +102,21 @@ export function ScheduleSheet() {
68
102
  const [customCron, setCustomCron] = useState(false)
69
103
 
70
104
  const editing = editingId ? schedules[editingId] : null
105
+ const isCreating = !editing
106
+ const steps = isCreating ? STEPS_CREATE : STEPS_EDIT
71
107
  const agentList = Object.values(agents).sort((a, b) => a.name.localeCompare(b.name))
72
108
 
109
+ // Compute which logical step we're on (template step only exists in create mode)
110
+ const templateStep = isCreating ? 0 : -1
111
+ const whatStep = isCreating ? 1 : 0
112
+ const whenStep = isCreating ? 2 : 1
113
+ const reviewStep = isCreating ? 3 : 2
114
+
73
115
  useEffect(() => {
74
116
  if (open) {
75
117
  loadAgents()
76
- setStep(0)
77
118
  if (editing) {
119
+ setStep(0)
78
120
  setName(editing.name || '')
79
121
  setAgentId(editing.agentId)
80
122
  setTaskPrompt(editing.taskPrompt)
@@ -83,7 +125,22 @@ export function ScheduleSheet() {
83
125
  setIntervalMs(editing.intervalMs || 3600000)
84
126
  setStatus(editing.status)
85
127
  setCustomCron(!CRON_PRESETS.some((p) => p.cron === editing.cron))
128
+ } else if (templatePrefill) {
129
+ // Opened from a quick-start card with pre-filled values
130
+ setName(templatePrefill.name)
131
+ setTaskPrompt(templatePrefill.taskPrompt)
132
+ setScheduleType(templatePrefill.scheduleType)
133
+ if (templatePrefill.cron) {
134
+ setCron(templatePrefill.cron)
135
+ setCustomCron(!CRON_PRESETS.some((p) => p.cron === templatePrefill.cron))
136
+ }
137
+ if (templatePrefill.intervalMs) setIntervalMs(templatePrefill.intervalMs)
138
+ setAgentId('')
139
+ setStatus('active')
140
+ setStep(1) // Skip template picker, go to "What" step
141
+ setTemplatePrefill(null)
86
142
  } else {
143
+ setStep(0) // Start at template picker
87
144
  setName('')
88
145
  setAgentId('')
89
146
  setTaskPrompt('')
@@ -153,15 +210,17 @@ export function ScheduleSheet() {
153
210
 
154
211
  {/* Step indicator */}
155
212
  <div className="flex items-center gap-2 mb-10">
156
- {STEPS.map((label, i) => (
213
+ {steps.map((label, i) => (
157
214
  <div key={label} className="flex items-center gap-2">
158
215
  {i > 0 && <div className={`w-8 h-px ${i <= step ? 'bg-accent-bright/40' : 'bg-white/[0.06]'}`} />}
159
216
  <button
160
217
  onClick={() => {
161
- // Allow going back, but only forward if valid
162
218
  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)
219
+ else if (i === step + 1) {
220
+ if (step === whatStep && step0Valid) setStep(i as Step)
221
+ else if (step === whenStep && step1Valid) setStep(i as Step)
222
+ else if (step === templateStep) setStep(i as Step)
223
+ }
165
224
  }}
166
225
  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
226
  ${i === step
@@ -189,8 +248,50 @@ export function ScheduleSheet() {
189
248
  ))}
190
249
  </div>
191
250
 
192
- {/* Step 0: What */}
193
- {step === 0 && (
251
+ {/* Template Picker (create only) */}
252
+ {step === templateStep && isCreating && (
253
+ <div>
254
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
255
+ {SCHEDULE_TEMPLATES.map((tpl) => {
256
+ const IconComp = TEMPLATE_ICONS[tpl.icon] || FileText
257
+ return (
258
+ <button
259
+ key={tpl.id}
260
+ onClick={() => {
261
+ const setters = { setName, setTaskPrompt, setScheduleType, setCron, setIntervalMs, setCustomCron }
262
+ applyTemplate(tpl, setters)
263
+ setStep(whatStep as Step)
264
+ }}
265
+ className="flex items-start gap-3.5 p-4 rounded-[14px] border border-white/[0.06] bg-surface
266
+ text-left cursor-pointer transition-all duration-200 hover:bg-surface-2 hover:border-white/[0.1]
267
+ active:scale-[0.98]"
268
+ style={{ fontFamily: 'inherit' }}
269
+ >
270
+ <div className="w-9 h-9 rounded-[10px] bg-accent-soft flex items-center justify-center shrink-0 mt-0.5">
271
+ <IconComp size={16} className="text-accent-bright" />
272
+ </div>
273
+ <div className="min-w-0">
274
+ <div className="text-[14px] font-600 text-text mb-0.5">{tpl.name}</div>
275
+ <div className="text-[12px] text-text-3/70 leading-[1.4]">{tpl.description}</div>
276
+ <div className="mt-1.5 text-[11px] text-text-3/40 capitalize">{tpl.category}</div>
277
+ </div>
278
+ </button>
279
+ )
280
+ })}
281
+ </div>
282
+ <button
283
+ onClick={() => setStep(whatStep as Step)}
284
+ className="w-full py-3.5 rounded-[14px] border border-dashed border-white/[0.08] bg-transparent
285
+ text-text-3 text-[14px] font-600 cursor-pointer transition-all hover:bg-surface hover:text-text-2 hover:border-white/[0.12]"
286
+ style={{ fontFamily: 'inherit' }}
287
+ >
288
+ Start from scratch
289
+ </button>
290
+ </div>
291
+ )}
292
+
293
+ {/* Step: What */}
294
+ {step === whatStep && (
194
295
  <div>
195
296
  <div className="mb-8">
196
297
  <SectionLabel>Name</SectionLabel>
@@ -221,8 +322,8 @@ export function ScheduleSheet() {
221
322
  </div>
222
323
  )}
223
324
 
224
- {/* Step 1: When */}
225
- {step === 1 && (
325
+ {/* Step: When */}
326
+ {step === whenStep && (
226
327
  <div>
227
328
  <div className="mb-8">
228
329
  <SectionLabel>Schedule Type</SectionLabel>
@@ -334,8 +435,8 @@ export function ScheduleSheet() {
334
435
  </div>
335
436
  )}
336
437
 
337
- {/* Step 2: Review */}
338
- {step === 2 && (
438
+ {/* Step: Review */}
439
+ {step === reviewStep && (
339
440
  <div className="mb-8">
340
441
  <div className="p-5 rounded-[16px] bg-surface border border-white/[0.06] space-y-4">
341
442
  <div>
@@ -381,7 +482,7 @@ export function ScheduleSheet() {
381
482
  Delete
382
483
  </button>
383
484
  )}
384
- {step > 0 && (
485
+ {step > (isCreating ? templateStep : 0) && step !== templateStep && (
385
486
  <button
386
487
  onClick={() => setStep((step - 1) as Step)}
387
488
  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"
@@ -391,17 +492,27 @@ export function ScheduleSheet() {
391
492
  </button>
392
493
  )}
393
494
  <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 ? (
495
+ {step !== templateStep && (
496
+ <button
497
+ onClick={onClose}
498
+ 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"
499
+ style={{ fontFamily: 'inherit' }}
500
+ >
501
+ Cancel
502
+ </button>
503
+ )}
504
+ {step === templateStep && isCreating ? (
505
+ <button
506
+ onClick={onClose}
507
+ 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"
508
+ style={{ fontFamily: 'inherit' }}
509
+ >
510
+ Cancel
511
+ </button>
512
+ ) : step < reviewStep ? (
402
513
  <button
403
514
  onClick={() => setStep((step + 1) as Step)}
404
- disabled={step === 0 ? !step0Valid : !step1Valid}
515
+ disabled={step === whatStep ? !step0Valid : !step1Valid}
405
516
  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
517
  style={{ fontFamily: 'inherit' }}
407
518
  >
@@ -161,7 +161,7 @@ export function SecretSheet() {
161
161
  }`}
162
162
  style={{ fontFamily: 'inherit' }}
163
163
  >
164
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />
164
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={24} />
165
165
  <span className="text-[13px] text-text flex-1 truncate">{agent.name}</span>
166
166
  {selected && (
167
167
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="text-accent-bright shrink-0">
@@ -102,7 +102,7 @@ export function SecretsList({ inSidebar }: Props) {
102
102
  <div className="flex items-center gap-1.5 mt-1.5 pl-[22px]">
103
103
  <div className="flex items-center -space-x-1.5">
104
104
  {scopedAgents.slice(0, 5).map((agent) => (
105
- <AgentAvatar key={agent.id} seed={agent.avatarSeed} name={agent.name} size={16} className="ring-1 ring-surface" />
105
+ <AgentAvatar key={agent.id} seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={16} className="ring-1 ring-surface" />
106
106
  ))}
107
107
  </div>
108
108
  {scopedAgents.length > 5 && (
@@ -86,7 +86,7 @@ export function SessionCard({ session, active, onClick }: Props) {
86
86
  <div className="flex items-center gap-2.5">
87
87
  {agent && (
88
88
  <div className="relative shrink-0">
89
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={24} />
89
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={24} />
90
90
  {(heartbeatEnabled || session.active) && (
91
91
  <span className="absolute -bottom-0.5 -right-0.5 w-2 h-2 rounded-full bg-emerald-400 ring-2 ring-[#0f0f1a]" />
92
92
  )}
@@ -72,7 +72,7 @@ export function AgentPickerList({
72
72
  {active && (
73
73
  <div className="absolute left-0 top-2 bottom-2 w-[2.5px] rounded-full bg-accent-bright" />
74
74
  )}
75
- <AgentAvatar seed={a.avatarSeed || null} name={a.name} size={28} />
75
+ <AgentAvatar seed={a.avatarSeed || null} avatarUrl={a.avatarUrl} name={a.name} size={28} />
76
76
  <span className={`text-[13px] font-600 flex-1 truncate ${active ? 'text-accent-bright' : 'text-text-2'}`}>
77
77
  {a.name}
78
78
  </span>
@@ -115,7 +115,7 @@ export function AgentSwitchDialog() {
115
115
  ${idx === selectedIdx ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'}`}
116
116
  style={{ fontFamily: 'inherit' }}
117
117
  >
118
- <AgentAvatar seed={agent.avatarSeed} name={agent.name} size={28} />
118
+ <AgentAvatar seed={agent.avatarSeed} avatarUrl={agent.avatarUrl} name={agent.name} size={28} />
119
119
  <div className="flex-1 min-w-0">
120
120
  <div className="flex items-center gap-2">
121
121
  <span className="text-[13px] font-500 text-text truncate">{agent.name}</span>
@@ -0,0 +1,237 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+
6
+ interface CommandItem {
7
+ id: string
8
+ label: string
9
+ category: 'agent' | 'chat' | 'task' | 'nav'
10
+ onSelect: () => void
11
+ }
12
+
13
+ export function CommandPalette() {
14
+ const [open, setOpen] = useState(false)
15
+ const [query, setQuery] = useState('')
16
+ const [selectedIndex, setSelectedIndex] = useState(0)
17
+ const inputRef = useRef<HTMLInputElement>(null)
18
+ const listRef = useRef<HTMLDivElement>(null)
19
+
20
+ const agents = useAppStore((s) => s.agents)
21
+ const sessions = useAppStore((s) => s.sessions)
22
+ const tasks = useAppStore((s) => s.tasks)
23
+ const setCurrentSession = useAppStore((s) => s.setCurrentSession)
24
+ const setActiveView = useAppStore((s) => s.setActiveView)
25
+ const setEditingAgentId = useAppStore((s) => s.setEditingAgentId)
26
+ const setAgentSheetOpen = useAppStore((s) => s.setAgentSheetOpen)
27
+ const setEditingTaskId = useAppStore((s) => s.setEditingTaskId)
28
+ const setTaskSheetOpen = useAppStore((s) => s.setTaskSheetOpen)
29
+
30
+ // Register keyboard shortcut
31
+ useEffect(() => {
32
+ const handler = (e: KeyboardEvent) => {
33
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
34
+ e.preventDefault()
35
+ setOpen((v) => !v)
36
+ }
37
+ if (e.key === 'Escape' && open) {
38
+ setOpen(false)
39
+ }
40
+ }
41
+ window.addEventListener('keydown', handler)
42
+ return () => window.removeEventListener('keydown', handler)
43
+ }, [open])
44
+
45
+ // Focus input when opened
46
+ useEffect(() => {
47
+ if (open) {
48
+ setQuery('')
49
+ setSelectedIndex(0)
50
+ setTimeout(() => inputRef.current?.focus(), 50)
51
+ }
52
+ }, [open])
53
+
54
+ const items = useMemo<CommandItem[]>(() => {
55
+ const result: CommandItem[] = []
56
+
57
+ // Navigation items
58
+ const views = ['agents', 'tasks', 'chatrooms', 'schedules', 'connectors', 'providers', 'secrets', 'settings', 'memory', 'skills'] as const
59
+ for (const v of views) {
60
+ result.push({
61
+ id: `nav:${v}`,
62
+ label: `Go to ${v}`,
63
+ category: 'nav',
64
+ onSelect: () => { setActiveView(v); setOpen(false) },
65
+ })
66
+ }
67
+
68
+ // Agents
69
+ for (const agent of Object.values(agents)) {
70
+ result.push({
71
+ id: `agent:${agent.id}`,
72
+ label: agent.name,
73
+ category: 'agent',
74
+ onSelect: () => { setEditingAgentId(agent.id); setAgentSheetOpen(true); setOpen(false) },
75
+ })
76
+ }
77
+
78
+ // Chats (sessions)
79
+ for (const session of Object.values(sessions)) {
80
+ if (session.name === '__main__') continue
81
+ result.push({
82
+ id: `chat:${session.id}`,
83
+ label: session.name || 'Untitled chat',
84
+ category: 'chat',
85
+ onSelect: () => { setCurrentSession(session.id); setActiveView('agents'); setOpen(false) },
86
+ })
87
+ }
88
+
89
+ // Tasks
90
+ for (const task of Object.values(tasks)) {
91
+ if (task.status === 'archived') continue
92
+ result.push({
93
+ id: `task:${task.id}`,
94
+ label: task.title,
95
+ category: 'task',
96
+ onSelect: () => { setEditingTaskId(task.id); setTaskSheetOpen(true); setOpen(false) },
97
+ })
98
+ }
99
+
100
+ return result
101
+ }, [agents, sessions, tasks, setActiveView, setCurrentSession, setEditingAgentId, setAgentSheetOpen, setEditingTaskId, setTaskSheetOpen])
102
+
103
+ const filtered = useMemo(() => {
104
+ if (!query.trim()) return items.slice(0, 20)
105
+ const q = query.toLowerCase()
106
+ return items
107
+ .filter((item) => item.label.toLowerCase().includes(q))
108
+ .slice(0, 20)
109
+ }, [items, query])
110
+
111
+ // Reset selection when results change
112
+ useEffect(() => { setSelectedIndex(0) }, [filtered])
113
+
114
+ const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
115
+ if (e.key === 'ArrowDown') {
116
+ e.preventDefault()
117
+ setSelectedIndex((i) => Math.min(i + 1, filtered.length - 1))
118
+ } else if (e.key === 'ArrowUp') {
119
+ e.preventDefault()
120
+ setSelectedIndex((i) => Math.max(i - 1, 0))
121
+ } else if (e.key === 'Enter' && filtered[selectedIndex]) {
122
+ e.preventDefault()
123
+ filtered[selectedIndex].onSelect()
124
+ }
125
+ }, [filtered, selectedIndex])
126
+
127
+ // Scroll selected item into view
128
+ useEffect(() => {
129
+ if (!listRef.current) return
130
+ const el = listRef.current.children[selectedIndex] as HTMLElement | undefined
131
+ el?.scrollIntoView({ block: 'nearest' })
132
+ }, [selectedIndex])
133
+
134
+ if (!open) return null
135
+
136
+ const categoryLabel = { agent: 'Agents', chat: 'Chats', task: 'Tasks', nav: 'Navigation' } as const
137
+ const categoryIcon = {
138
+ agent: (
139
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
140
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" />
141
+ </svg>
142
+ ),
143
+ chat: (
144
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
145
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
146
+ </svg>
147
+ ),
148
+ task: (
149
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
150
+ <path d="M9 11l3 3L22 4" /><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
151
+ </svg>
152
+ ),
153
+ nav: (
154
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
155
+ <circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
156
+ </svg>
157
+ ),
158
+ }
159
+
160
+ // Group by category
161
+ const grouped = new Map<string, CommandItem[]>()
162
+ for (const item of filtered) {
163
+ const group = grouped.get(item.category) || []
164
+ group.push(item)
165
+ grouped.set(item.category, group)
166
+ }
167
+
168
+ let flatIndex = 0
169
+
170
+ return (
171
+ <div className="fixed inset-0 z-[200] flex items-start justify-center pt-[15vh]">
172
+ <div className="absolute inset-0 bg-black/70 backdrop-blur-sm" onClick={() => setOpen(false)} />
173
+ <div
174
+ className="relative w-full max-w-[520px] mx-4 bg-raised rounded-[16px] border border-white/[0.08] shadow-[0_24px_80px_rgba(0,0,0,0.6)] overflow-hidden"
175
+ style={{ animation: 'fade-in 0.15s ease' }}
176
+ >
177
+ {/* Search input */}
178
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-white/[0.06]">
179
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-text-3 shrink-0">
180
+ <circle cx="11" cy="11" r="8" /><line x1="21" y1="21" x2="16.65" y2="16.65" />
181
+ </svg>
182
+ <input
183
+ ref={inputRef}
184
+ value={query}
185
+ onChange={(e) => setQuery(e.target.value)}
186
+ onKeyDown={handleKeyDown}
187
+ placeholder="Search agents, chats, tasks..."
188
+ className="flex-1 bg-transparent border-none outline-none text-[14px] text-text-1 placeholder:text-text-3/50"
189
+ />
190
+ <kbd className="hidden md:inline-flex items-center px-1.5 py-0.5 rounded-[6px] bg-white/[0.06] text-[11px] text-text-3 font-500">
191
+ esc
192
+ </kbd>
193
+ </div>
194
+
195
+ {/* Results */}
196
+ <div ref={listRef} className="max-h-[360px] overflow-y-auto py-2">
197
+ {filtered.length === 0 ? (
198
+ <div className="px-4 py-8 text-center text-[13px] text-text-3/50">No results found</div>
199
+ ) : (
200
+ Array.from(grouped.entries()).map(([category, groupItems]) => (
201
+ <div key={category}>
202
+ <div className="px-4 py-1.5 text-[11px] font-600 text-text-3/50 uppercase tracking-wider">
203
+ {categoryLabel[category as keyof typeof categoryLabel]}
204
+ </div>
205
+ {groupItems.map((item) => {
206
+ const idx = flatIndex++
207
+ return (
208
+ <button
209
+ key={item.id}
210
+ onClick={item.onSelect}
211
+ className={`w-full flex items-center gap-3 px-4 py-2.5 text-left border-none cursor-pointer transition-colors
212
+ ${idx === selectedIndex ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-2 hover:bg-white/[0.04]'}`}
213
+ style={{ fontFamily: 'inherit' }}
214
+ >
215
+ <span className="shrink-0 text-text-3">{categoryIcon[item.category as keyof typeof categoryIcon]}</span>
216
+ <span className="text-[13px] font-500 truncate">{item.label}</span>
217
+ </button>
218
+ )
219
+ })}
220
+ </div>
221
+ ))
222
+ )}
223
+ </div>
224
+
225
+ {/* Footer hint */}
226
+ <div className="px-4 py-2 border-t border-white/[0.06] flex items-center gap-4 text-[11px] text-text-3/40">
227
+ <span className="flex items-center gap-1">
228
+ <kbd className="px-1 py-0.5 rounded bg-white/[0.06] text-[10px]">&uarr;&darr;</kbd> navigate
229
+ </span>
230
+ <span className="flex items-center gap-1">
231
+ <kbd className="px-1 py-0.5 rounded bg-white/[0.06] text-[10px]">&crarr;</kbd> select
232
+ </span>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ )
237
+ }
@@ -23,6 +23,7 @@ export const CONNECTOR_PLATFORM_META: Record<ConnectorPlatform, { label: string;
23
23
  teams: { label: 'Teams', color: '#6264A7' },
24
24
  googlechat: { label: 'Google Chat', color: '#00AC47' },
25
25
  matrix: { label: 'Matrix', color: '#0DBD8B' },
26
+ email: { label: 'Email', color: '#EA4335' },
26
27
  }
27
28
 
28
29
  export function getConnectorPlatformLabel(platform: ConnectorPlatform): string {