@swarmclawai/swarmclaw 0.3.0 → 0.4.0
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 +20 -11
- package/bin/server-cmd.js +14 -7
- package/bin/swarmclaw.js +3 -1
- package/bin/update-cmd.js +120 -0
- package/next.config.ts +2 -0
- package/package.json +3 -1
- package/src/app/api/agents/[id]/route.ts +3 -0
- package/src/app/api/agents/[id]/thread/route.ts +2 -1
- package/src/app/api/agents/route.ts +5 -1
- package/src/app/api/auth/route.ts +3 -1
- package/src/app/api/claude-skills/route.ts +3 -1
- package/src/app/api/connectors/[id]/route.ts +4 -0
- package/src/app/api/connectors/route.ts +6 -1
- package/src/app/api/credentials/route.ts +3 -1
- package/src/app/api/daemon/route.ts +6 -1
- package/src/app/api/ip/route.ts +3 -1
- package/src/app/api/mcp-servers/route.ts +3 -1
- package/src/app/api/orchestrator/graph/route.ts +25 -0
- package/src/app/api/plugins/marketplace/route.ts +3 -1
- package/src/app/api/plugins/route.ts +3 -1
- package/src/app/api/providers/[id]/route.ts +3 -0
- package/src/app/api/providers/configs/route.ts +3 -1
- package/src/app/api/providers/route.ts +5 -1
- package/src/app/api/schedules/[id]/route.ts +3 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/route.ts +3 -1
- package/src/app/api/sessions/[id]/chat/route.ts +5 -2
- package/src/app/api/sessions/route.ts +9 -2
- package/src/app/api/settings/route.ts +3 -1
- package/src/app/api/setup/doctor/route.ts +1 -0
- package/src/app/api/setup/openclaw-device/route.ts +3 -1
- package/src/app/api/skills/route.ts +3 -1
- package/src/app/api/tasks/[id]/approve/route.ts +73 -0
- package/src/app/api/tasks/[id]/route.ts +3 -0
- package/src/app/api/tasks/route.ts +3 -0
- package/src/app/api/usage/route.ts +3 -1
- package/src/app/api/version/route.ts +3 -1
- package/src/app/api/webhooks/[id]/route.ts +2 -1
- package/src/app/api/webhooks/route.ts +3 -1
- package/src/app/icon.svg +58 -0
- package/src/app/page.tsx +8 -2
- package/src/cli/index.js +1 -9
- package/src/cli/index.ts +51 -1
- package/src/cli/spec.js +0 -8
- package/src/components/agents/agent-card.tsx +1 -1
- package/src/components/agents/agent-sheet.tsx +63 -80
- package/src/components/chat/chat-area.tsx +44 -30
- package/src/components/chat/chat-tool-toggles.tsx +12 -53
- package/src/components/chat/message-bubble.tsx +110 -42
- package/src/components/chat/tool-call-bubble.tsx +41 -3
- package/src/components/chat/tool-request-banner.tsx +1 -9
- package/src/components/connectors/connector-list.tsx +3 -8
- package/src/components/connectors/connector-sheet.tsx +24 -29
- package/src/components/input/chat-input.tsx +72 -56
- package/src/components/knowledge/knowledge-list.tsx +27 -31
- package/src/components/layout/app-layout.tsx +92 -71
- package/src/components/layout/daemon-indicator.tsx +3 -5
- package/src/components/logs/log-list.tsx +5 -9
- package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
- package/src/components/memory/memory-detail.tsx +1 -1
- package/src/components/plugins/plugin-list.tsx +227 -27
- package/src/components/providers/provider-list.tsx +46 -13
- package/src/components/providers/provider-sheet.tsx +0 -45
- package/src/components/runs/run-list.tsx +6 -15
- package/src/components/schedules/schedule-card.tsx +54 -4
- package/src/components/schedules/schedule-list.tsx +6 -3
- package/src/components/schedules/schedule-sheet.tsx +0 -47
- package/src/components/secrets/secrets-list.tsx +20 -2
- package/src/components/sessions/new-session-sheet.tsx +8 -9
- package/src/components/shared/connector-platform-icon.tsx +22 -20
- package/src/components/shared/model-combobox.tsx +148 -0
- package/src/components/shared/settings/section-heartbeat.tsx +7 -39
- package/src/components/shared/settings/section-orchestrator.tsx +8 -9
- package/src/components/skills/skill-list.tsx +260 -34
- package/src/components/skills/skill-sheet.tsx +0 -45
- package/src/components/tasks/task-board.tsx +3 -6
- package/src/components/tasks/task-card.tsx +43 -1
- package/src/components/tasks/task-list.tsx +3 -5
- package/src/components/tasks/task-sheet.tsx +0 -44
- package/src/components/usage/usage-list.tsx +12 -4
- package/src/hooks/use-ws.ts +66 -0
- package/src/instrumentation.ts +2 -0
- package/src/lib/chat.ts +14 -2
- package/src/lib/providers/anthropic.ts +1 -1
- package/src/lib/providers/index.ts +2 -0
- package/src/lib/providers/ollama.ts +1 -1
- package/src/lib/providers/openai.ts +33 -12
- package/src/lib/server/chat-execution.ts +19 -4
- package/src/lib/server/connectors/manager.ts +9 -3
- package/src/lib/server/context-manager.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -0
- package/src/lib/server/data-dir.ts +1 -0
- package/src/lib/server/heartbeat-service.ts +67 -3
- package/src/lib/server/langgraph-checkpoint.ts +274 -0
- package/src/lib/server/main-agent-loop.ts +61 -2
- package/src/lib/server/orchestrator-lg.ts +394 -13
- package/src/lib/server/orchestrator.ts +25 -5
- package/src/lib/server/queue.ts +17 -3
- package/src/lib/server/session-run-manager.ts +6 -1
- package/src/lib/server/session-tools/delegate.ts +2 -2
- package/src/lib/server/session-tools/index.ts +2 -0
- package/src/lib/server/session-tools/sandbox.ts +164 -0
- package/src/lib/server/storage-mcp.test.ts +25 -2
- package/src/lib/server/storage.ts +24 -7
- package/src/lib/server/stream-agent-chat.ts +77 -22
- package/src/lib/server/task-validation.test.ts +23 -0
- package/src/lib/server/task-validation.ts +5 -3
- package/src/lib/server/ws-hub.ts +85 -0
- package/src/lib/tool-definitions.ts +42 -0
- package/src/lib/upload.ts +7 -1
- package/src/lib/ws-client.ts +124 -0
- package/src/stores/use-chat-store.ts +33 -13
- package/src/types/index.ts +8 -1
- package/src/app/api/agents/generate/route.ts +0 -42
- package/src/app/api/generate/info/route.ts +0 -12
- package/src/app/api/generate/route.ts +0 -106
- package/src/app/favicon.ico +0 -0
- package/src/components/shared/ai-gen-block.tsx +0 -77
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
import { useEffect, useState, useMemo } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { createSchedule, updateSchedule, deleteSchedule } from '@/lib/schedules'
|
|
6
|
-
import { api } from '@/lib/api-client'
|
|
7
6
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
8
|
-
import { AiGenBlock } from '@/components/shared/ai-gen-block'
|
|
9
7
|
import type { ScheduleType, ScheduleStatus } from '@/types'
|
|
10
8
|
import cronstrue from 'cronstrue'
|
|
11
9
|
|
|
@@ -63,49 +61,12 @@ export function ScheduleSheet() {
|
|
|
63
61
|
const [status, setStatus] = useState<ScheduleStatus>('active')
|
|
64
62
|
const [customCron, setCustomCron] = useState(false)
|
|
65
63
|
|
|
66
|
-
// AI generation state
|
|
67
|
-
const [aiPrompt, setAiPrompt] = useState('')
|
|
68
|
-
const [generating, setGenerating] = useState(false)
|
|
69
|
-
const [generated, setGenerated] = useState(false)
|
|
70
|
-
const [genError, setGenError] = useState('')
|
|
71
|
-
const appSettings = useAppStore((s) => s.appSettings)
|
|
72
|
-
const loadSettings = useAppStore((s) => s.loadSettings)
|
|
73
|
-
|
|
74
64
|
const editing = editingId ? schedules[editingId] : null
|
|
75
65
|
const agentList = Object.values(agents)
|
|
76
66
|
|
|
77
|
-
const handleGenerate = async () => {
|
|
78
|
-
if (!aiPrompt.trim()) return
|
|
79
|
-
setGenerating(true)
|
|
80
|
-
setGenError('')
|
|
81
|
-
try {
|
|
82
|
-
const result = await api<{ name?: string; taskPrompt?: string; scheduleType?: ScheduleType; cron?: string; intervalMs?: number; error?: string }>('POST', '/generate', { type: 'schedule', prompt: aiPrompt })
|
|
83
|
-
if (result.error) {
|
|
84
|
-
setGenError(result.error)
|
|
85
|
-
} else if (result.name || result.taskPrompt) {
|
|
86
|
-
if (result.name) setName(result.name)
|
|
87
|
-
if (result.taskPrompt) setTaskPrompt(result.taskPrompt)
|
|
88
|
-
if (result.scheduleType) setScheduleType(result.scheduleType)
|
|
89
|
-
if (result.cron) { setCron(result.cron); setCustomCron(true) }
|
|
90
|
-
if (result.intervalMs) setIntervalMs(result.intervalMs)
|
|
91
|
-
setGenerated(true)
|
|
92
|
-
} else {
|
|
93
|
-
setGenError('AI returned empty response — try again')
|
|
94
|
-
}
|
|
95
|
-
} catch (err: unknown) {
|
|
96
|
-
setGenError(err instanceof Error ? err.message : 'Generation failed')
|
|
97
|
-
}
|
|
98
|
-
setGenerating(false)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
67
|
useEffect(() => {
|
|
102
68
|
if (open) {
|
|
103
69
|
loadAgents()
|
|
104
|
-
loadSettings()
|
|
105
|
-
setAiPrompt('')
|
|
106
|
-
setGenerating(false)
|
|
107
|
-
setGenerated(false)
|
|
108
|
-
setGenError('')
|
|
109
70
|
if (editing) {
|
|
110
71
|
setName(editing.name || '')
|
|
111
72
|
setAgentId(editing.agentId)
|
|
@@ -175,14 +136,6 @@ export function ScheduleSheet() {
|
|
|
175
136
|
<p className="text-[14px] text-text-3">Automate agent tasks on a schedule</p>
|
|
176
137
|
</div>
|
|
177
138
|
|
|
178
|
-
{/* AI Generation */}
|
|
179
|
-
{!editing && <AiGenBlock
|
|
180
|
-
aiPrompt={aiPrompt} setAiPrompt={setAiPrompt}
|
|
181
|
-
generating={generating} generated={generated} genError={genError}
|
|
182
|
-
onGenerate={handleGenerate} appSettings={appSettings}
|
|
183
|
-
placeholder='Describe the schedule, e.g. "Run keyword research every Monday at 9am"'
|
|
184
|
-
/>}
|
|
185
|
-
|
|
186
139
|
<div className="mb-8">
|
|
187
140
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Name</label>
|
|
188
141
|
<input type="text" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Daily keyword research" className={inputClass} style={{ fontFamily: 'inherit' }} />
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
|
+
import { api } from '@/lib/api-client'
|
|
5
6
|
|
|
6
7
|
interface Props {
|
|
7
8
|
inSidebar?: boolean
|
|
@@ -18,6 +19,12 @@ export function SecretsList({ inSidebar }: Props) {
|
|
|
18
19
|
loadSecrets()
|
|
19
20
|
}, [])
|
|
20
21
|
|
|
22
|
+
const handleDelete = async (e: React.MouseEvent, id: string) => {
|
|
23
|
+
e.stopPropagation()
|
|
24
|
+
await api('DELETE', `/secrets/${id}`)
|
|
25
|
+
loadSecrets()
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
const secretList = Object.values(secrets)
|
|
22
29
|
|
|
23
30
|
if (!secretList.length) {
|
|
@@ -44,7 +51,7 @@ export function SecretsList({ inSidebar }: Props) {
|
|
|
44
51
|
|
|
45
52
|
return (
|
|
46
53
|
<div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-5 pb-6'}`}>
|
|
47
|
-
<div className=
|
|
54
|
+
<div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
|
|
48
55
|
{secretList.map((secret) => {
|
|
49
56
|
const scopeLabel = secret.scope === 'global'
|
|
50
57
|
? 'All orchestrators'
|
|
@@ -68,7 +75,18 @@ export function SecretsList({ inSidebar }: Props) {
|
|
|
68
75
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
|
69
76
|
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
70
77
|
</svg>
|
|
71
|
-
<span className="text-[14px] font-600 text-text truncate">{secret.name}</span>
|
|
78
|
+
<span className="text-[14px] font-600 text-text truncate flex-1">{secret.name}</span>
|
|
79
|
+
{!inSidebar && (
|
|
80
|
+
<button
|
|
81
|
+
onClick={(e) => handleDelete(e, secret.id)}
|
|
82
|
+
className="text-text-3/40 hover:text-red-400 transition-colors p-0.5 shrink-0"
|
|
83
|
+
title="Delete"
|
|
84
|
+
>
|
|
85
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
86
|
+
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
|
87
|
+
</svg>
|
|
88
|
+
</button>
|
|
89
|
+
)}
|
|
72
90
|
</div>
|
|
73
91
|
<div className="flex items-center gap-2 pl-[22px]">
|
|
74
92
|
<span className="text-[11px] font-mono text-text-3">{secret.service}</span>
|
|
@@ -7,6 +7,7 @@ import { createSession, createCredential } from '@/lib/sessions'
|
|
|
7
7
|
import { BottomSheet } from '@/components/shared/bottom-sheet'
|
|
8
8
|
import { DirBrowser } from '@/components/shared/dir-browser'
|
|
9
9
|
import { TOOL_LABELS, TOOL_DESCRIPTIONS } from '@/components/chat/tool-call-bubble'
|
|
10
|
+
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
10
11
|
import type { ProviderType, SessionTool } from '@/types'
|
|
11
12
|
|
|
12
13
|
export function NewSessionSheet() {
|
|
@@ -269,16 +270,14 @@ export function NewSessionSheet() {
|
|
|
269
270
|
<label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">
|
|
270
271
|
Model
|
|
271
272
|
</label>
|
|
272
|
-
<
|
|
273
|
+
<ModelCombobox
|
|
274
|
+
providerId={currentProvider.id}
|
|
273
275
|
value={model}
|
|
274
|
-
onChange={
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
<option key={m} value={m}>{m}</option>
|
|
280
|
-
))}
|
|
281
|
-
</select>
|
|
276
|
+
onChange={setModel}
|
|
277
|
+
models={currentProvider.models}
|
|
278
|
+
defaultModels={currentProvider.defaultModels}
|
|
279
|
+
className={`${inputClass} cursor-pointer`}
|
|
280
|
+
/>
|
|
282
281
|
</div>
|
|
283
282
|
)}
|
|
284
283
|
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Connector, ConnectorPlatform, Session } from '@/types'
|
|
2
2
|
import { cn } from '@/lib/utils'
|
|
3
|
+
import { BsMicrosoftTeams } from 'react-icons/bs'
|
|
4
|
+
import {
|
|
5
|
+
SiDiscord,
|
|
6
|
+
SiGooglechat,
|
|
7
|
+
SiMatrix,
|
|
8
|
+
SiSignal,
|
|
9
|
+
SiSlack,
|
|
10
|
+
SiTelegram,
|
|
11
|
+
SiWhatsapp,
|
|
12
|
+
} from 'react-icons/si'
|
|
3
13
|
|
|
4
14
|
export const CONNECTOR_PLATFORM_META: Record<ConnectorPlatform, { label: string; color: string }> = {
|
|
5
15
|
discord: { label: 'Discord', color: '#5865F2' },
|
|
@@ -45,29 +55,21 @@ export function ConnectorPlatformIcon({
|
|
|
45
55
|
}: ConnectorPlatformIconProps) {
|
|
46
56
|
switch (platform) {
|
|
47
57
|
case 'discord':
|
|
48
|
-
return
|
|
49
|
-
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
|
|
50
|
-
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
|
|
51
|
-
</svg>
|
|
52
|
-
)
|
|
58
|
+
return <SiDiscord size={size} className={className} />
|
|
53
59
|
case 'telegram':
|
|
54
|
-
return
|
|
55
|
-
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
|
|
56
|
-
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
|
57
|
-
</svg>
|
|
58
|
-
)
|
|
60
|
+
return <SiTelegram size={size} className={className} />
|
|
59
61
|
case 'slack':
|
|
60
|
-
return
|
|
61
|
-
<svg width={size} height={size} viewBox="0 0 24 24" fill="currentColor" className={className}>
|
|
62
|
-
<path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z" />
|
|
63
|
-
</svg>
|
|
64
|
-
)
|
|
62
|
+
return <SiSlack size={size} className={className} />
|
|
65
63
|
case 'whatsapp':
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
return <SiWhatsapp size={size} className={className} />
|
|
65
|
+
case 'signal':
|
|
66
|
+
return <SiSignal size={size} className={className} />
|
|
67
|
+
case 'googlechat':
|
|
68
|
+
return <SiGooglechat size={size} className={className} />
|
|
69
|
+
case 'matrix':
|
|
70
|
+
return <SiMatrix size={size} className={className} />
|
|
71
|
+
case 'teams':
|
|
72
|
+
return <BsMicrosoftTeams size={size} className={className} />
|
|
71
73
|
case 'openclaw':
|
|
72
74
|
return (
|
|
73
75
|
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className}>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
4
|
+
import { api } from '@/lib/api-client'
|
|
5
|
+
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
+
|
|
7
|
+
interface ModelComboboxProps {
|
|
8
|
+
providerId: string
|
|
9
|
+
value: string
|
|
10
|
+
onChange: (model: string) => void
|
|
11
|
+
models: string[]
|
|
12
|
+
defaultModels?: string[]
|
|
13
|
+
className?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function ModelCombobox({
|
|
17
|
+
providerId,
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
models,
|
|
21
|
+
defaultModels = [],
|
|
22
|
+
className,
|
|
23
|
+
}: ModelComboboxProps) {
|
|
24
|
+
const [open, setOpen] = useState(false)
|
|
25
|
+
const [query, setQuery] = useState('')
|
|
26
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
27
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
28
|
+
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
29
|
+
|
|
30
|
+
const filtered = query
|
|
31
|
+
? models.filter((m) => m.toLowerCase().includes(query.toLowerCase()))
|
|
32
|
+
: models
|
|
33
|
+
|
|
34
|
+
const isCustom = (m: string) => defaultModels.length > 0 && !defaultModels.includes(m)
|
|
35
|
+
const showAdd = query && !models.some((m) => m.toLowerCase() === query.toLowerCase())
|
|
36
|
+
|
|
37
|
+
const persistModels = useCallback(async (next: string[]) => {
|
|
38
|
+
await api('PUT', `/providers/${providerId}/models`, { models: next })
|
|
39
|
+
await loadProviders()
|
|
40
|
+
}, [providerId, loadProviders])
|
|
41
|
+
|
|
42
|
+
const addModel = useCallback(async (name: string) => {
|
|
43
|
+
const next = [...models, name]
|
|
44
|
+
await persistModels(next)
|
|
45
|
+
onChange(name)
|
|
46
|
+
setQuery('')
|
|
47
|
+
setOpen(false)
|
|
48
|
+
}, [models, persistModels, onChange])
|
|
49
|
+
|
|
50
|
+
const removeModel = useCallback(async (name: string, e: React.MouseEvent) => {
|
|
51
|
+
e.stopPropagation()
|
|
52
|
+
const next = models.filter((m) => m !== name)
|
|
53
|
+
if (value === name) onChange(next[0] || '')
|
|
54
|
+
if (next.length === defaultModels.length && next.every((m) => defaultModels.includes(m))) {
|
|
55
|
+
await api('DELETE', `/providers/${providerId}/models`)
|
|
56
|
+
} else {
|
|
57
|
+
await persistModels(next)
|
|
58
|
+
}
|
|
59
|
+
await loadProviders()
|
|
60
|
+
}, [models, defaultModels, value, onChange, providerId, persistModels, loadProviders])
|
|
61
|
+
|
|
62
|
+
const selectModel = useCallback((m: string) => {
|
|
63
|
+
onChange(m)
|
|
64
|
+
setQuery('')
|
|
65
|
+
setOpen(false)
|
|
66
|
+
}, [onChange])
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
const handler = (e: MouseEvent) => {
|
|
70
|
+
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
71
|
+
setOpen(false)
|
|
72
|
+
setQuery('')
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
document.addEventListener('mousedown', handler)
|
|
76
|
+
return () => document.removeEventListener('mousedown', handler)
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div ref={containerRef} className="relative">
|
|
81
|
+
<div
|
|
82
|
+
className={`flex items-center ${className || ''}`}
|
|
83
|
+
onClick={() => {
|
|
84
|
+
setOpen(true)
|
|
85
|
+
setTimeout(() => inputRef.current?.focus(), 0)
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
<input
|
|
89
|
+
ref={inputRef}
|
|
90
|
+
type="text"
|
|
91
|
+
value={open ? query : value}
|
|
92
|
+
onChange={(e) => {
|
|
93
|
+
setQuery(e.target.value)
|
|
94
|
+
if (!open) setOpen(true)
|
|
95
|
+
}}
|
|
96
|
+
onFocus={() => setOpen(true)}
|
|
97
|
+
placeholder={value || 'Select model…'}
|
|
98
|
+
className="w-full bg-transparent outline-none text-inherit placeholder:text-text-3/50"
|
|
99
|
+
style={{ fontFamily: 'inherit' }}
|
|
100
|
+
/>
|
|
101
|
+
<svg className="w-4 h-4 text-text-3 shrink-0 ml-2" viewBox="0 0 16 16" fill="none">
|
|
102
|
+
<path d="M4 6l4 4 4-4" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
|
103
|
+
</svg>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
{open && (
|
|
107
|
+
<div className="absolute z-50 top-full left-0 right-0 mt-1 max-h-[240px] overflow-y-auto rounded-[12px] border border-white/[0.08] bg-surface-2 shadow-xl">
|
|
108
|
+
{filtered.map((m) => (
|
|
109
|
+
<div
|
|
110
|
+
key={m}
|
|
111
|
+
onClick={() => selectModel(m)}
|
|
112
|
+
className={`flex items-center justify-between px-3 py-2 text-[14px] cursor-pointer transition-colors hover:bg-white/[0.04] ${m === value ? 'text-accent-bright' : 'text-text'}`}
|
|
113
|
+
>
|
|
114
|
+
<span className="truncate">{m}</span>
|
|
115
|
+
{isCustom(m) && (
|
|
116
|
+
<button
|
|
117
|
+
onClick={(e) => removeModel(m, e)}
|
|
118
|
+
className="ml-2 p-0.5 rounded hover:bg-white/[0.08] text-text-3 hover:text-red-400 transition-colors shrink-0"
|
|
119
|
+
title="Remove custom model"
|
|
120
|
+
>
|
|
121
|
+
<svg className="w-3.5 h-3.5" viewBox="0 0 16 16" fill="none">
|
|
122
|
+
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
123
|
+
</svg>
|
|
124
|
+
</button>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
))}
|
|
128
|
+
|
|
129
|
+
{showAdd && (
|
|
130
|
+
<div
|
|
131
|
+
onClick={() => addModel(query.trim())}
|
|
132
|
+
className="flex items-center gap-2 px-3 py-2 text-[14px] cursor-pointer transition-colors hover:bg-white/[0.04] text-accent-bright border-t border-white/[0.06]"
|
|
133
|
+
>
|
|
134
|
+
<svg className="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none">
|
|
135
|
+
<path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
136
|
+
</svg>
|
|
137
|
+
<span className="truncate">Add “{query.trim()}”</span>
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{filtered.length === 0 && !showAdd && (
|
|
142
|
+
<div className="px-3 py-2 text-[14px] text-text-3">No models found</div>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
)
|
|
148
|
+
}
|
|
@@ -35,49 +35,20 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
|
|
|
35
35
|
return (
|
|
36
36
|
<div className="mb-10">
|
|
37
37
|
<h3 className="font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-2">
|
|
38
|
-
Heartbeat
|
|
38
|
+
Heartbeat Defaults
|
|
39
39
|
</h3>
|
|
40
40
|
<p className="text-[12px] text-text-3 mb-5">
|
|
41
|
-
|
|
41
|
+
Global defaults inherited by agents. Enable heartbeat and set interval/model per-agent in the agent editor.
|
|
42
42
|
</p>
|
|
43
43
|
<div className="p-6 rounded-[18px] bg-surface border border-white/[0.06]">
|
|
44
44
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
|
|
45
45
|
<div>
|
|
46
|
-
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">
|
|
47
|
-
<input
|
|
48
|
-
type="text"
|
|
49
|
-
value={appSettings.heartbeatInterval ?? appSettings.heartbeatIntervalSec ?? '30m'}
|
|
50
|
-
onChange={(e) => {
|
|
51
|
-
const val = e.target.value.trim()
|
|
52
|
-
patchSettings({ heartbeatInterval: val || null })
|
|
53
|
-
}}
|
|
54
|
-
placeholder="30m"
|
|
55
|
-
className={inputClass}
|
|
56
|
-
style={{ fontFamily: 'inherit' }}
|
|
57
|
-
/>
|
|
58
|
-
<p className="text-[11px] text-text-3/60 mt-2">Duration string (e.g. 30m, 1h) or seconds. Set to 0 to disable.</p>
|
|
59
|
-
</div>
|
|
60
|
-
<div>
|
|
61
|
-
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Heartbeat Prompt</label>
|
|
46
|
+
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Default Prompt</label>
|
|
62
47
|
<input
|
|
63
48
|
type="text"
|
|
64
49
|
value={appSettings.heartbeatPrompt || ''}
|
|
65
50
|
onChange={(e) => patchSettings({ heartbeatPrompt: e.target.value || null })}
|
|
66
|
-
placeholder="Leave blank for default"
|
|
67
|
-
className={inputClass}
|
|
68
|
-
style={{ fontFamily: 'inherit' }}
|
|
69
|
-
/>
|
|
70
|
-
</div>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mb-5">
|
|
74
|
-
<div>
|
|
75
|
-
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Heartbeat Model</label>
|
|
76
|
-
<input
|
|
77
|
-
type="text"
|
|
78
|
-
value={appSettings.heartbeatModel || ''}
|
|
79
|
-
onChange={(e) => patchSettings({ heartbeatModel: e.target.value || null })}
|
|
80
|
-
placeholder="Leave blank for session default"
|
|
51
|
+
placeholder="Leave blank for built-in default"
|
|
81
52
|
className={inputClass}
|
|
82
53
|
style={{ fontFamily: 'inherit' }}
|
|
83
54
|
/>
|
|
@@ -142,20 +113,17 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
|
|
|
142
113
|
</div>
|
|
143
114
|
|
|
144
115
|
<div>
|
|
145
|
-
<
|
|
146
|
-
Internal ping text used for ongoing sessions. Leave blank to use the default.
|
|
147
|
-
</p>
|
|
148
|
-
<div className="mt-4 flex items-center gap-2.5">
|
|
116
|
+
<div className="flex items-center gap-2.5">
|
|
149
117
|
<button
|
|
150
118
|
onClick={handleDisableAllHeartbeats}
|
|
151
119
|
disabled={disablingHeartbeats}
|
|
152
120
|
className="px-3.5 py-2 rounded-[10px] border border-rose-400/25 bg-rose-500/10 text-rose-300 hover:bg-rose-500/16 transition-colors cursor-pointer disabled:opacity-60 disabled:cursor-not-allowed text-[12px] font-600"
|
|
153
121
|
style={{ fontFamily: 'inherit' }}
|
|
154
122
|
>
|
|
155
|
-
{disablingHeartbeats ? 'Stopping\u2026' : 'Stop All
|
|
123
|
+
{disablingHeartbeats ? 'Stopping\u2026' : 'Stop All Heartbeats'}
|
|
156
124
|
</button>
|
|
157
125
|
<span className="text-[11px] text-text-3/70">
|
|
158
|
-
Disables heartbeat on every
|
|
126
|
+
Disables heartbeat on every agent and cancels queued runs.
|
|
159
127
|
</span>
|
|
160
128
|
</div>
|
|
161
129
|
{heartbeatBulkNotice && (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useAppStore } from '@/stores/use-app-store'
|
|
4
|
+
import { ModelCombobox } from '@/components/shared/model-combobox'
|
|
4
5
|
import type { SettingsSectionProps } from './types'
|
|
5
6
|
|
|
6
7
|
const NON_LANGGRAPH_PROVIDER_IDS = new Set(['claude-cli', 'codex-cli', 'opencode-cli'])
|
|
@@ -53,16 +54,14 @@ export function OrchestratorSection({ appSettings, patchSettings, inputClass }:
|
|
|
53
54
|
{lgProviderInfo && lgProviderInfo.models.length > 0 && (
|
|
54
55
|
<div className="mb-5">
|
|
55
56
|
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-3">Model</label>
|
|
56
|
-
<
|
|
57
|
+
<ModelCombobox
|
|
58
|
+
providerId={lgProviderInfo.id}
|
|
57
59
|
value={appSettings.langGraphModel || lgProviderInfo.models[0]}
|
|
58
|
-
onChange={(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<option key={m} value={m}>{m}</option>
|
|
64
|
-
))}
|
|
65
|
-
</select>
|
|
60
|
+
onChange={(m) => patchSettings({ langGraphModel: m })}
|
|
61
|
+
models={lgProviderInfo.models}
|
|
62
|
+
defaultModels={lgProviderInfo.defaultModels}
|
|
63
|
+
className={`${inputClass} cursor-pointer`}
|
|
64
|
+
/>
|
|
66
65
|
</div>
|
|
67
66
|
)}
|
|
68
67
|
|