@swarmclawai/swarmclaw 0.7.3 → 0.7.4
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 +47 -40
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +17 -0
- package/src/app/api/agents/[id]/thread/route.ts +3 -1
- package/src/app/api/agents/route.ts +23 -1
- package/src/app/api/auth/route.ts +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/route.ts +12 -0
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +1 -1
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +6 -10
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +2 -1
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/page.tsx +126 -15
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +34 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +20 -4
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-sheet.tsx +249 -7
- package/src/components/agents/inspector-panel.tsx +3 -2
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +41 -14
- package/src/components/chat/chat-card.tsx +2 -1
- package/src/components/chat/chat-header.tsx +8 -13
- package/src/components/chat/chat-list.tsx +58 -20
- package/src/components/chat/message-list.tsx +142 -18
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +157 -86
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +2 -0
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/projects/project-detail.tsx +7 -2
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/section-heartbeat.tsx +11 -6
- package/src/components/shared/settings/section-orchestrator.tsx +3 -0
- package/src/components/shared/settings/settings-page.tsx +5 -3
- package/src/components/tasks/approvals-panel.tsx +7 -1
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approvals-auto-approve.test.ts +59 -0
- package/src/lib/server/build-llm.test.ts +13 -5
- package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
- package/src/lib/server/chat-execution.ts +159 -71
- package/src/lib/server/chatroom-helpers.test.ts +7 -0
- package/src/lib/server/chatroom-helpers.ts +99 -6
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/manager.ts +89 -61
- package/src/lib/server/connectors/slack.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -2
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +10 -4
- package/src/lib/server/main-agent-loop.ts +13 -6
- package/src/lib/server/openclaw-exec-config.ts +4 -2
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/orchestrator-lg.ts +1 -2
- package/src/lib/server/orchestrator.ts +3 -2
- package/src/lib/server/plugins.test.ts +9 -1
- package/src/lib/server/plugins.ts +12 -2
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +1 -1
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
- package/src/lib/server/session-tools/crud.ts +27 -3
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +18 -8
- package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
- package/src/lib/server/session-tools/file.ts +8 -2
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/index.ts +31 -1
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/monitor.ts +14 -7
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +9 -2
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/session-info.ts +22 -1
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +3 -1
- package/src/lib/server/session-tools/web.ts +73 -30
- package/src/lib/server/storage.ts +29 -3
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +139 -4
- package/src/lib/server/structured-extract.ts +1 -1
- package/src/lib/server/task-mention.ts +0 -1
- package/src/lib/server/tool-aliases.ts +37 -6
- package/src/lib/server/tool-capability-policy.ts +1 -1
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.ts +55 -1
- package/src/stores/use-app-store.ts +43 -1
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +189 -6
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogDescription,
|
|
7
|
+
DialogFooter,
|
|
8
|
+
DialogHeader,
|
|
9
|
+
DialogTitle,
|
|
10
|
+
} from '@/components/ui/dialog'
|
|
11
|
+
|
|
3
12
|
interface Props {
|
|
4
13
|
open: boolean
|
|
5
14
|
title: string
|
|
@@ -11,37 +20,43 @@ interface Props {
|
|
|
11
20
|
}
|
|
12
21
|
|
|
13
22
|
export function ConfirmDialog({ open, title, message, confirmLabel = 'Confirm', danger, onConfirm, onCancel }: Props) {
|
|
14
|
-
if (!open) return null
|
|
15
|
-
|
|
16
23
|
return (
|
|
17
|
-
<
|
|
18
|
-
<
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
>
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
<Dialog open={open} onOpenChange={(nextOpen) => { if (!nextOpen) onCancel() }}>
|
|
25
|
+
<DialogContent
|
|
26
|
+
className="sm:max-w-[400px] rounded-[20px] border-white/[0.06] bg-raised p-0 shadow-[0_24px_80px_rgba(0,0,0,0.55)]"
|
|
27
|
+
>
|
|
28
|
+
<div className="p-6">
|
|
29
|
+
<DialogHeader className="text-left">
|
|
30
|
+
<DialogTitle className="font-display text-[18px] font-700 tracking-[-0.02em] text-text">
|
|
31
|
+
{title}
|
|
32
|
+
</DialogTitle>
|
|
33
|
+
<DialogDescription className="mt-2 text-[13px] leading-relaxed text-text-2">
|
|
34
|
+
{message}
|
|
35
|
+
</DialogDescription>
|
|
36
|
+
</DialogHeader>
|
|
37
|
+
<DialogFooter className="mt-6">
|
|
38
|
+
<button
|
|
39
|
+
type="button"
|
|
40
|
+
onClick={onCancel}
|
|
41
|
+
className="flex-1 rounded-[12px] border border-white/[0.06] bg-transparent px-4 py-2.5 text-[13px] font-600 text-text-2 transition-all duration-200 hover:bg-surface"
|
|
42
|
+
style={{ fontFamily: 'inherit' }}
|
|
43
|
+
>
|
|
44
|
+
Cancel
|
|
45
|
+
</button>
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
onClick={onConfirm}
|
|
49
|
+
className={`flex-1 rounded-[12px] border-none px-4 py-2.5 text-[13px] font-600 text-white transition-all duration-200 active:scale-[0.98]
|
|
50
|
+
${danger
|
|
51
|
+
? 'bg-danger shadow-[0_4px_20px_rgba(244,63,94,0.2)]'
|
|
52
|
+
: 'bg-accent-bright shadow-[0_4px_20px_rgba(99,102,241,0.2)]'}`}
|
|
53
|
+
style={{ fontFamily: 'inherit' }}
|
|
54
|
+
>
|
|
55
|
+
{confirmLabel}
|
|
56
|
+
</button>
|
|
57
|
+
</DialogFooter>
|
|
43
58
|
</div>
|
|
44
|
-
</
|
|
45
|
-
</
|
|
59
|
+
</DialogContent>
|
|
60
|
+
</Dialog>
|
|
46
61
|
)
|
|
47
62
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
3
|
+
import { useState, useRef, useEffect, useCallback, useEffectEvent } from 'react'
|
|
4
4
|
import { api } from '@/lib/api-client'
|
|
5
5
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
|
+
import { fetchProviderModelDiscovery } from '@/lib/provider-model-discovery-client'
|
|
6
7
|
|
|
7
8
|
interface ModelComboboxProps {
|
|
8
9
|
providerId: string
|
|
@@ -10,6 +11,9 @@ interface ModelComboboxProps {
|
|
|
10
11
|
onChange: (model: string) => void
|
|
11
12
|
models: string[]
|
|
12
13
|
defaultModels?: string[]
|
|
14
|
+
credentialId?: string | null
|
|
15
|
+
apiEndpoint?: string | null
|
|
16
|
+
supportsDiscovery?: boolean
|
|
13
17
|
className?: string
|
|
14
18
|
}
|
|
15
19
|
|
|
@@ -19,20 +23,31 @@ export function ModelCombobox({
|
|
|
19
23
|
onChange,
|
|
20
24
|
models,
|
|
21
25
|
defaultModels = [],
|
|
26
|
+
credentialId,
|
|
27
|
+
apiEndpoint,
|
|
28
|
+
supportsDiscovery = true,
|
|
22
29
|
className,
|
|
23
30
|
}: ModelComboboxProps) {
|
|
24
31
|
const [open, setOpen] = useState(false)
|
|
25
32
|
const [query, setQuery] = useState('')
|
|
33
|
+
const [discoveredModels, setDiscoveredModels] = useState<string[]>([])
|
|
34
|
+
const [discoveryState, setDiscoveryState] = useState<'idle' | 'loading' | 'ready' | 'notice'>('idle')
|
|
35
|
+
const [discoveryMessage, setDiscoveryMessage] = useState('')
|
|
36
|
+
const [discoveryCached, setDiscoveryCached] = useState(false)
|
|
26
37
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
27
38
|
const containerRef = useRef<HTMLDivElement>(null)
|
|
39
|
+
const lastDiscoveryKeyRef = useRef<string | null>(null)
|
|
28
40
|
const loadProviders = useAppStore((s) => s.loadProviders)
|
|
29
41
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
42
|
+
const availableModels = [...models, ...discoveredModels].filter((model, index, source) => source.indexOf(model) === index)
|
|
43
|
+
const trimmedQuery = query.trim()
|
|
44
|
+
const filtered = trimmedQuery
|
|
45
|
+
? availableModels.filter((m) => m.toLowerCase().includes(trimmedQuery.toLowerCase()))
|
|
46
|
+
: availableModels
|
|
33
47
|
|
|
34
|
-
const isCustom = (m: string) => defaultModels.length > 0 && !defaultModels.includes(m)
|
|
35
|
-
const showAdd =
|
|
48
|
+
const isCustom = (m: string) => models.includes(m) && defaultModels.length > 0 && !defaultModels.includes(m)
|
|
49
|
+
const showAdd = trimmedQuery && !availableModels.some((m) => m.toLowerCase() === trimmedQuery.toLowerCase())
|
|
50
|
+
const discoveryKey = `${providerId}::${credentialId || ''}::${apiEndpoint?.trim() || ''}`
|
|
36
51
|
|
|
37
52
|
const persistModels = useCallback(async (next: string[]) => {
|
|
38
53
|
await api('PUT', `/providers/${providerId}/models`, { models: next })
|
|
@@ -65,6 +80,43 @@ export function ModelCombobox({
|
|
|
65
80
|
setOpen(false)
|
|
66
81
|
}, [onChange])
|
|
67
82
|
|
|
83
|
+
const loadDiscoveredModels = useCallback(async (force = false) => {
|
|
84
|
+
if (!supportsDiscovery) return
|
|
85
|
+
if (!force && lastDiscoveryKeyRef.current === discoveryKey) return
|
|
86
|
+
setDiscoveryState('loading')
|
|
87
|
+
setDiscoveryMessage('')
|
|
88
|
+
try {
|
|
89
|
+
const result = await fetchProviderModelDiscovery({
|
|
90
|
+
providerId,
|
|
91
|
+
credentialId,
|
|
92
|
+
endpoint: apiEndpoint,
|
|
93
|
+
force,
|
|
94
|
+
})
|
|
95
|
+
lastDiscoveryKeyRef.current = discoveryKey
|
|
96
|
+
setDiscoveryCached(result.cached)
|
|
97
|
+
setDiscoveredModels(result.models)
|
|
98
|
+
setDiscoveryState(result.ok ? 'ready' : 'notice')
|
|
99
|
+
setDiscoveryMessage(result.message || '')
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const message = error instanceof Error ? error.message : 'Failed to load live models.'
|
|
102
|
+
setDiscoveryState('notice')
|
|
103
|
+
setDiscoveryMessage(message)
|
|
104
|
+
}
|
|
105
|
+
}, [apiEndpoint, credentialId, discoveryKey, providerId, supportsDiscovery])
|
|
106
|
+
|
|
107
|
+
const resetDiscoveryState = useEffectEvent(() => {
|
|
108
|
+
lastDiscoveryKeyRef.current = null
|
|
109
|
+
setDiscoveredModels([])
|
|
110
|
+
setDiscoveryState('idle')
|
|
111
|
+
setDiscoveryMessage('')
|
|
112
|
+
setDiscoveryCached(false)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const syncDiscoveryOnOpen = useEffectEvent(() => {
|
|
116
|
+
if (!open || !supportsDiscovery) return
|
|
117
|
+
void loadDiscoveredModels()
|
|
118
|
+
})
|
|
119
|
+
|
|
68
120
|
useEffect(() => {
|
|
69
121
|
const handler = (e: MouseEvent) => {
|
|
70
122
|
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
|
@@ -76,6 +128,14 @@ export function ModelCombobox({
|
|
|
76
128
|
return () => document.removeEventListener('mousedown', handler)
|
|
77
129
|
}, [])
|
|
78
130
|
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
resetDiscoveryState()
|
|
133
|
+
}, [providerId, credentialId, apiEndpoint])
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
syncDiscoveryOnOpen()
|
|
137
|
+
}, [open, supportsDiscovery, loadDiscoveredModels])
|
|
138
|
+
|
|
79
139
|
return (
|
|
80
140
|
<div ref={containerRef} className="relative">
|
|
81
141
|
<div
|
|
@@ -105,6 +165,28 @@ export function ModelCombobox({
|
|
|
105
165
|
|
|
106
166
|
{open && (
|
|
107
167
|
<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">
|
|
168
|
+
{supportsDiscovery && (
|
|
169
|
+
<div className="sticky top-0 z-[1] flex items-center justify-between gap-3 border-b border-white/[0.06] bg-surface-2/95 px-3 py-2 backdrop-blur">
|
|
170
|
+
<div className={`min-w-0 text-[11px] ${discoveryState === 'notice' ? 'text-text-3/80' : 'text-text-3/60'}`}>
|
|
171
|
+
{discoveryState === 'loading'
|
|
172
|
+
? 'Checking live model catalog...'
|
|
173
|
+
: discoveryMessage || 'Type any model name or load the live catalog.'}
|
|
174
|
+
{discoveryCached && discoveryState === 'ready' ? ' Cached.' : ''}
|
|
175
|
+
</div>
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
onClick={(e) => {
|
|
179
|
+
e.stopPropagation()
|
|
180
|
+
void loadDiscoveredModels(true)
|
|
181
|
+
}}
|
|
182
|
+
disabled={discoveryState === 'loading'}
|
|
183
|
+
className="shrink-0 rounded-[7px] border border-white/[0.08] bg-white/[0.03] px-2 py-1 text-[10px] font-600 text-text-3/80 transition-colors hover:bg-white/[0.06] hover:text-text disabled:cursor-default disabled:opacity-60"
|
|
184
|
+
>
|
|
185
|
+
{discoveryState === 'loading' ? 'Loading...' : discoveredModels.length > 0 ? 'Refresh' : 'Fetch live'}
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
|
|
108
190
|
{filtered.map((m) => (
|
|
109
191
|
<div
|
|
110
192
|
key={m}
|
|
@@ -128,13 +210,13 @@ export function ModelCombobox({
|
|
|
128
210
|
|
|
129
211
|
{showAdd && (
|
|
130
212
|
<div
|
|
131
|
-
onClick={() => addModel(
|
|
213
|
+
onClick={() => addModel(trimmedQuery)}
|
|
132
214
|
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
215
|
>
|
|
134
216
|
<svg className="w-3.5 h-3.5 shrink-0" viewBox="0 0 16 16" fill="none">
|
|
135
217
|
<path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
|
|
136
218
|
</svg>
|
|
137
|
-
<span className="truncate">Add “{
|
|
219
|
+
<span className="truncate">Add “{trimmedQuery}”</span>
|
|
138
220
|
</div>
|
|
139
221
|
)}
|
|
140
222
|
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_HEARTBEAT_ACK_MAX_CHARS,
|
|
5
|
+
DEFAULT_HEARTBEAT_SHOW_ALERTS,
|
|
6
|
+
DEFAULT_HEARTBEAT_SHOW_OK,
|
|
7
|
+
} from '@/lib/heartbeat-defaults'
|
|
3
8
|
import { useState } from 'react'
|
|
4
9
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
10
|
import { api } from '@/lib/api-client'
|
|
@@ -63,10 +68,10 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
|
|
|
63
68
|
<input
|
|
64
69
|
type="number"
|
|
65
70
|
min={0}
|
|
66
|
-
value={appSettings.heartbeatAckMaxChars ??
|
|
71
|
+
value={appSettings.heartbeatAckMaxChars ?? DEFAULT_HEARTBEAT_ACK_MAX_CHARS}
|
|
67
72
|
onChange={(e) => {
|
|
68
73
|
const n = Number.parseInt(e.target.value, 10)
|
|
69
|
-
patchSettings({ heartbeatAckMaxChars: Number.isFinite(n) ? Math.max(0, n) :
|
|
74
|
+
patchSettings({ heartbeatAckMaxChars: Number.isFinite(n) ? Math.max(0, n) : DEFAULT_HEARTBEAT_ACK_MAX_CHARS })
|
|
70
75
|
}}
|
|
71
76
|
className={inputClass}
|
|
72
77
|
style={{ fontFamily: 'inherit' }}
|
|
@@ -79,7 +84,7 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
|
|
|
79
84
|
<div>
|
|
80
85
|
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Show OK Messages</label>
|
|
81
86
|
<button
|
|
82
|
-
onClick={() => patchSettings({ heartbeatShowOk: !(appSettings.heartbeatShowOk ??
|
|
87
|
+
onClick={() => patchSettings({ heartbeatShowOk: !(appSettings.heartbeatShowOk ?? DEFAULT_HEARTBEAT_SHOW_OK) })}
|
|
83
88
|
className={`px-3 py-2 rounded-[10px] border text-[12px] font-600 transition-colors cursor-pointer ${
|
|
84
89
|
appSettings.heartbeatShowOk
|
|
85
90
|
? 'border-emerald-400/25 bg-emerald-500/10 text-emerald-300'
|
|
@@ -93,15 +98,15 @@ export function HeartbeatSection({ appSettings, patchSettings, inputClass }: Set
|
|
|
93
98
|
<div>
|
|
94
99
|
<label className="block font-display text-[11px] font-600 text-text-3 uppercase tracking-[0.08em] mb-2">Show Alert Messages</label>
|
|
95
100
|
<button
|
|
96
|
-
onClick={() => patchSettings({ heartbeatShowAlerts: !(appSettings.heartbeatShowAlerts ??
|
|
101
|
+
onClick={() => patchSettings({ heartbeatShowAlerts: !(appSettings.heartbeatShowAlerts ?? DEFAULT_HEARTBEAT_SHOW_ALERTS) })}
|
|
97
102
|
className={`px-3 py-2 rounded-[10px] border text-[12px] font-600 transition-colors cursor-pointer ${
|
|
98
|
-
(appSettings.heartbeatShowAlerts ??
|
|
103
|
+
(appSettings.heartbeatShowAlerts ?? DEFAULT_HEARTBEAT_SHOW_ALERTS)
|
|
99
104
|
? 'border-emerald-400/25 bg-emerald-500/10 text-emerald-300'
|
|
100
105
|
: 'border-white/[0.08] bg-white/[0.03] text-text-3'
|
|
101
106
|
}`}
|
|
102
107
|
style={{ fontFamily: 'inherit' }}
|
|
103
108
|
>
|
|
104
|
-
{(appSettings.heartbeatShowAlerts ??
|
|
109
|
+
{(appSettings.heartbeatShowAlerts ?? DEFAULT_HEARTBEAT_SHOW_ALERTS) ? 'On' : 'Off'}
|
|
105
110
|
</button>
|
|
106
111
|
</div>
|
|
107
112
|
<div>
|
|
@@ -67,6 +67,9 @@ export function OrchestratorSection({ appSettings, patchSettings, inputClass }:
|
|
|
67
67
|
onChange={(m) => patchSettings({ langGraphModel: m })}
|
|
68
68
|
models={lgProviderInfo.models}
|
|
69
69
|
defaultModels={lgProviderInfo.defaultModels}
|
|
70
|
+
credentialId={appSettings.langGraphCredentialId}
|
|
71
|
+
apiEndpoint={appSettings.langGraphEndpoint}
|
|
72
|
+
supportsDiscovery={lgProviderInfo.supportsModelDiscovery}
|
|
70
73
|
className={`${inputClass} cursor-pointer`}
|
|
71
74
|
/>
|
|
72
75
|
</div>
|
|
@@ -117,8 +117,9 @@ export function SettingsPage() {
|
|
|
117
117
|
const [searchQuery, setSearchQuery] = useState('')
|
|
118
118
|
const credList = Object.values(credentials)
|
|
119
119
|
const patchSettings = updateSettings
|
|
120
|
-
const
|
|
121
|
-
|
|
120
|
+
const sections = useMemo<SettingsSectionDef[]>(() => {
|
|
121
|
+
const sectionProps = { appSettings, patchSettings, inputClass }
|
|
122
|
+
return [
|
|
122
123
|
{
|
|
123
124
|
id: 'user-preferences',
|
|
124
125
|
tabId: 'general',
|
|
@@ -223,7 +224,8 @@ export function SettingsPage() {
|
|
|
223
224
|
keywords: ['secrets', 'credentials', 'api keys', 'tokens'],
|
|
224
225
|
render: () => <SecretsSection {...sectionProps} />,
|
|
225
226
|
},
|
|
226
|
-
|
|
227
|
+
]
|
|
228
|
+
}, [appSettings, credList, patchSettings])
|
|
227
229
|
const sectionsByTab = useMemo(() => {
|
|
228
230
|
const map = new Map<string, SettingsSectionDef[]>()
|
|
229
231
|
for (const section of sections) {
|
|
@@ -72,6 +72,12 @@ export function ApprovalsPanel() {
|
|
|
72
72
|
const [search, setSearch] = useState('')
|
|
73
73
|
const [scope, setScope] = useState<ApprovalScope>('all')
|
|
74
74
|
const [categoryFilter, setCategoryFilter] = useState('all')
|
|
75
|
+
const [now, setNow] = useState(() => Date.now())
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
const intervalId = window.setInterval(() => setNow(Date.now()), 60_000)
|
|
79
|
+
return () => window.clearInterval(intervalId)
|
|
80
|
+
}, [])
|
|
75
81
|
|
|
76
82
|
const taskApprovals = useMemo(() => {
|
|
77
83
|
return Object.values(tasks)
|
|
@@ -168,7 +174,7 @@ export function ApprovalsPanel() {
|
|
|
168
174
|
},
|
|
169
175
|
{
|
|
170
176
|
label: 'Recently Active',
|
|
171
|
-
value: workflowApprovals.filter((req) =>
|
|
177
|
+
value: workflowApprovals.filter((req) => now - req.updatedAt < 60 * 60 * 1000).length,
|
|
172
178
|
tone: 'text-emerald-400',
|
|
173
179
|
hint: 'Updated in the last hour',
|
|
174
180
|
},
|
|
@@ -39,7 +39,7 @@ function DialogOverlay({
|
|
|
39
39
|
<DialogPrimitive.Overlay
|
|
40
40
|
data-slot="dialog-overlay"
|
|
41
41
|
className={cn(
|
|
42
|
-
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/
|
|
42
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/72 backdrop-blur-md",
|
|
43
43
|
className
|
|
44
44
|
)}
|
|
45
45
|
{...props}
|
|
@@ -71,7 +71,7 @@ function DialogContent({
|
|
|
71
71
|
{showCloseButton && (
|
|
72
72
|
<DialogPrimitive.Close
|
|
73
73
|
data-slot="dialog-close"
|
|
74
|
-
className="
|
|
74
|
+
className="absolute top-4 right-4 inline-flex h-9 w-9 items-center justify-center rounded-[12px] border border-white/[0.06] bg-white/[0.03] text-text-3 transition-all hover:bg-white/[0.06] hover:text-text-2 focus:outline-none focus:ring-2 focus:ring-accent-bright/30 disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
75
75
|
>
|
|
76
76
|
<XIcon />
|
|
77
77
|
<span className="sr-only">Close</span>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback } from 'react'
|
|
4
4
|
import { api } from '@/lib/api-client'
|
|
5
|
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
|
5
6
|
import type { WalletTransaction } from '@/types'
|
|
6
7
|
|
|
7
8
|
interface WalletApprovalDialogProps {
|
|
@@ -35,65 +36,69 @@ export function WalletApprovalDialog({ transaction, walletAddress, onClose, onRe
|
|
|
35
36
|
const amountSol = transaction.amountLamports / 1e9
|
|
36
37
|
|
|
37
38
|
return (
|
|
38
|
-
<
|
|
39
|
-
<
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
<Dialog open onOpenChange={(nextOpen) => { if (!nextOpen) onClose() }}>
|
|
40
|
+
<DialogContent className="sm:max-w-[460px] rounded-[20px] border-white/[0.08] bg-surface/95 p-0 shadow-[0_24px_80px_rgba(0,0,0,0.6)]">
|
|
41
|
+
<div className="p-6 space-y-5">
|
|
42
|
+
<DialogHeader className="text-left">
|
|
43
|
+
<div className="flex items-center gap-2">
|
|
44
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400">
|
|
45
|
+
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
46
|
+
<line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
|
|
47
|
+
</svg>
|
|
48
|
+
<DialogTitle className="font-display text-[16px] font-700 tracking-[-0.02em] text-text-1">
|
|
49
|
+
Transaction Approval
|
|
50
|
+
</DialogTitle>
|
|
51
|
+
</div>
|
|
52
|
+
<DialogDescription className="text-[12px] leading-relaxed text-text-3">
|
|
53
|
+
Crypto transactions are irreversible. Verify the recipient address carefully before approving.
|
|
54
|
+
</DialogDescription>
|
|
55
|
+
</DialogHeader>
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<div>
|
|
55
|
-
<span className="text-[11px] text-text-3/70 uppercase tracking-wide block mb-1">From</span>
|
|
56
|
-
<code className="text-[10px] text-text-3 font-mono break-all">{walletAddress}</code>
|
|
57
|
-
</div>
|
|
58
|
-
<div>
|
|
59
|
-
<span className="text-[11px] text-text-3/70 uppercase tracking-wide block mb-1">To</span>
|
|
60
|
-
<code className="text-[10px] text-text-3 font-mono break-all">{transaction.toAddress}</code>
|
|
61
|
-
</div>
|
|
62
|
-
{transaction.memo && (
|
|
57
|
+
<div className="rounded-[14px] border border-white/[0.06] bg-black/20 p-4 space-y-3">
|
|
58
|
+
<div className="flex items-center justify-between">
|
|
59
|
+
<span className="text-[11px] uppercase tracking-wide text-text-3/70">Amount</span>
|
|
60
|
+
<span className="text-[16px] font-600 text-text-1">{amountSol.toFixed(4)} SOL</span>
|
|
61
|
+
</div>
|
|
63
62
|
<div>
|
|
64
|
-
<span className="text-[11px] text-text-3/70
|
|
65
|
-
<
|
|
63
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-text-3/70">From</span>
|
|
64
|
+
<code className="text-[10px] text-text-3 font-mono break-all">{walletAddress}</code>
|
|
66
65
|
</div>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
<div>
|
|
67
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-text-3/70">To</span>
|
|
68
|
+
<code className="text-[10px] text-text-3 font-mono break-all">{transaction.toAddress}</code>
|
|
69
|
+
</div>
|
|
70
|
+
{transaction.memo && (
|
|
71
|
+
<div>
|
|
72
|
+
<span className="mb-1 block text-[11px] uppercase tracking-wide text-text-3/70">Reason</span>
|
|
73
|
+
<p className="text-[12px] text-text-2">{transaction.memo}</p>
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
73
77
|
|
|
74
|
-
|
|
78
|
+
{error && <p className="text-[11px] text-red-400">{error}</p>}
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
80
|
+
<DialogFooter>
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => handleDecision('deny')}
|
|
84
|
+
disabled={submitting}
|
|
85
|
+
className="flex-1 rounded-[12px] border border-white/[0.08] bg-surface px-4 py-2.5 text-[12px] font-600 text-text-3 transition-colors hover:border-red-400/30 hover:text-red-400 disabled:opacity-50"
|
|
86
|
+
style={{ fontFamily: 'inherit' }}
|
|
87
|
+
>
|
|
88
|
+
Deny
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={() => handleDecision('approve')}
|
|
93
|
+
disabled={submitting}
|
|
94
|
+
className="flex-1 rounded-[12px] bg-accent px-4 py-2.5 text-[12px] font-600 text-white transition-all hover:brightness-110 disabled:opacity-50"
|
|
95
|
+
style={{ fontFamily: 'inherit' }}
|
|
96
|
+
>
|
|
97
|
+
{submitting ? 'Processing...' : 'Approve & Send'}
|
|
98
|
+
</button>
|
|
99
|
+
</DialogFooter>
|
|
95
100
|
</div>
|
|
96
|
-
</
|
|
97
|
-
</
|
|
101
|
+
</DialogContent>
|
|
102
|
+
</Dialog>
|
|
98
103
|
)
|
|
99
104
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const DEFAULT_HEARTBEAT_INTERVAL_SEC = 1800
|
|
2
|
+
export const DEFAULT_HEARTBEAT_ACK_MAX_CHARS = 300
|
|
3
|
+
export const DEFAULT_HEARTBEAT_SHOW_OK = false
|
|
4
|
+
export const DEFAULT_HEARTBEAT_SHOW_ALERTS = true
|
|
5
|
+
|
|
6
|
+
function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
|
|
7
|
+
const parsed = typeof value === 'number'
|
|
8
|
+
? value
|
|
9
|
+
: typeof value === 'string'
|
|
10
|
+
? Number.parseInt(value, 10)
|
|
11
|
+
: Number.NaN
|
|
12
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
13
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function parseBoolSetting(value: unknown, fallback: boolean): boolean {
|
|
17
|
+
if (typeof value === 'boolean') return value
|
|
18
|
+
if (typeof value === 'string') {
|
|
19
|
+
const normalized = value.trim().toLowerCase()
|
|
20
|
+
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true
|
|
21
|
+
if (['0', 'false', 'no', 'off'].includes(normalized)) return false
|
|
22
|
+
}
|
|
23
|
+
return fallback
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface NormalizedHeartbeatSettingFields {
|
|
27
|
+
heartbeatIntervalSec: number
|
|
28
|
+
heartbeatAckMaxChars: number
|
|
29
|
+
heartbeatShowOk: boolean
|
|
30
|
+
heartbeatShowAlerts: boolean
|
|
31
|
+
heartbeatTarget: string | null
|
|
32
|
+
heartbeatPrompt: string | null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function normalizeHeartbeatSettingFields(settings: Record<string, unknown>): NormalizedHeartbeatSettingFields {
|
|
36
|
+
return {
|
|
37
|
+
heartbeatIntervalSec: parseIntSetting(settings.heartbeatIntervalSec, DEFAULT_HEARTBEAT_INTERVAL_SEC, 0, 86_400),
|
|
38
|
+
heartbeatAckMaxChars: parseIntSetting(settings.heartbeatAckMaxChars, DEFAULT_HEARTBEAT_ACK_MAX_CHARS, 0, 8_000),
|
|
39
|
+
heartbeatShowOk: parseBoolSetting(settings.heartbeatShowOk, DEFAULT_HEARTBEAT_SHOW_OK),
|
|
40
|
+
heartbeatShowAlerts: parseBoolSetting(settings.heartbeatShowAlerts, DEFAULT_HEARTBEAT_SHOW_ALERTS),
|
|
41
|
+
heartbeatTarget: typeof settings.heartbeatTarget === 'string' && settings.heartbeatTarget.trim()
|
|
42
|
+
? settings.heartbeatTarget.trim()
|
|
43
|
+
: null,
|
|
44
|
+
heartbeatPrompt: typeof settings.heartbeatPrompt === 'string' && settings.heartbeatPrompt.trim()
|
|
45
|
+
? settings.heartbeatPrompt.trim()
|
|
46
|
+
: null,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { MemoryEntry } from '@/types'
|
|
2
|
+
|
|
3
|
+
export type MemoryTier = 'working' | 'durable' | 'archive'
|
|
4
|
+
export type MemoryScopeBadge = 'global' | 'agent' | 'shared' | 'session' | 'project'
|
|
5
|
+
|
|
6
|
+
const WORKING_CATEGORIES = new Set(['execution', 'working', 'scratch', 'breadcrumb'])
|
|
7
|
+
const ARCHIVE_CATEGORIES = new Set(['session_archive'])
|
|
8
|
+
|
|
9
|
+
function hasProjectRoot(entry: Pick<MemoryEntry, 'metadata' | 'references' | 'filePaths'>): boolean {
|
|
10
|
+
const metadataRoot = typeof entry.metadata?.projectRoot === 'string' ? entry.metadata.projectRoot.trim() : ''
|
|
11
|
+
if (metadataRoot) return true
|
|
12
|
+
|
|
13
|
+
if (Array.isArray(entry.references)) {
|
|
14
|
+
for (const ref of entry.references) {
|
|
15
|
+
if (typeof ref.projectRoot === 'string' && ref.projectRoot.trim()) return true
|
|
16
|
+
if ((ref.type === 'project' || ref.type === 'folder' || ref.type === 'file') && typeof ref.path === 'string' && ref.path.trim()) {
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (Array.isArray(entry.filePaths)) {
|
|
23
|
+
for (const ref of entry.filePaths) {
|
|
24
|
+
if (typeof ref.projectRoot === 'string' && ref.projectRoot.trim()) return true
|
|
25
|
+
if (typeof ref.path === 'string' && ref.path.trim()) return true
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return false
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getMemoryTierForCategory(category: unknown): MemoryTier {
|
|
33
|
+
const normalized = typeof category === 'string' ? category.trim().toLowerCase() : ''
|
|
34
|
+
if (ARCHIVE_CATEGORIES.has(normalized)) return 'archive'
|
|
35
|
+
if (WORKING_CATEGORIES.has(normalized)) return 'working'
|
|
36
|
+
return 'durable'
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getMemoryTier(entry: Pick<MemoryEntry, 'category' | 'metadata'>): MemoryTier {
|
|
40
|
+
const metadataTier = typeof entry.metadata?.tier === 'string' ? entry.metadata.tier.trim().toLowerCase() : ''
|
|
41
|
+
if (metadataTier === 'working' || metadataTier === 'durable' || metadataTier === 'archive') {
|
|
42
|
+
return metadataTier
|
|
43
|
+
}
|
|
44
|
+
if (metadataTier === 'session_archive') return 'archive'
|
|
45
|
+
return getMemoryTierForCategory(entry.category)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function deriveMemoryScope(entry: Pick<MemoryEntry, 'agentId' | 'sessionId' | 'sharedWith' | 'metadata' | 'references' | 'filePaths'>): MemoryScopeBadge {
|
|
49
|
+
if (entry.sessionId) return 'session'
|
|
50
|
+
if (hasProjectRoot(entry)) return 'project'
|
|
51
|
+
if (entry.agentId && Array.isArray(entry.sharedWith) && entry.sharedWith.length > 0) return 'shared'
|
|
52
|
+
if (entry.agentId) return 'agent'
|
|
53
|
+
return 'global'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function getMemoryScopeLabel(scope: MemoryScopeBadge): string {
|
|
57
|
+
if (scope === 'agent') return 'private'
|
|
58
|
+
return scope
|
|
59
|
+
}
|