@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.
- package/README.md +62 -30
- package/package.json +10 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +34 -2
- package/src/app/api/chatrooms/route.ts +26 -3
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +44 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +20 -0
- package/src/cli/index.ts +223 -39
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +38 -6
- package/src/components/agents/agent-chat-list.tsx +79 -3
- package/src/components/agents/agent-sheet.tsx +191 -26
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +24 -9
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +17 -16
- package/src/components/chat/message-list.tsx +6 -5
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +165 -23
- package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +25 -3
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +35 -4
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +112 -17
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +76 -4
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +86 -12
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +239 -5
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +17 -5
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +238 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +8 -2
- package/src/lib/server/session-tools/memory.ts +23 -4
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +158 -6
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +15 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +98 -2
- 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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
48
|
-
|
|
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
|
-
{
|
|
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 ===
|
|
164
|
-
|
|
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
|
-
{/*
|
|
193
|
-
{step ===
|
|
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
|
|
225
|
-
{step ===
|
|
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
|
|
338
|
-
{step ===
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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 ===
|
|
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]">↑↓</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]">↵</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 {
|