@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.
Files changed (147) hide show
  1. package/README.md +47 -40
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +17 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +3 -1
  12. package/src/app/api/agents/route.ts +23 -1
  13. package/src/app/api/auth/route.ts +1 -1
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/route.ts +12 -0
  19. package/src/app/api/chats/heartbeat/route.ts +2 -1
  20. package/src/app/api/chats/route.ts +7 -1
  21. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  22. package/src/app/api/external-agents/[id]/route.ts +31 -0
  23. package/src/app/api/external-agents/register/route.ts +3 -0
  24. package/src/app/api/external-agents/route.ts +66 -0
  25. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  26. package/src/app/api/gateways/[id]/route.ts +79 -0
  27. package/src/app/api/gateways/route.ts +57 -0
  28. package/src/app/api/openclaw/gateway/route.ts +10 -7
  29. package/src/app/api/openclaw/skills/route.ts +1 -1
  30. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  31. package/src/app/api/schedules/[id]/route.ts +38 -9
  32. package/src/app/api/schedules/route.ts +51 -28
  33. package/src/app/api/settings/route.ts +6 -10
  34. package/src/app/api/setup/doctor/route.ts +6 -4
  35. package/src/app/api/tasks/[id]/route.ts +2 -1
  36. package/src/app/api/tasks/bulk/route.ts +2 -2
  37. package/src/app/page.tsx +126 -15
  38. package/src/cli/binary.test.js +142 -0
  39. package/src/cli/index.js +34 -11
  40. package/src/cli/index.test.js +195 -0
  41. package/src/cli/index.ts +20 -4
  42. package/src/cli/server-cmd.test.js +59 -0
  43. package/src/cli/spec.js +20 -2
  44. package/src/components/agents/agent-sheet.tsx +249 -7
  45. package/src/components/agents/inspector-panel.tsx +3 -2
  46. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  47. package/src/components/auth/setup-wizard.tsx +970 -275
  48. package/src/components/chat/chat-area.tsx +41 -14
  49. package/src/components/chat/chat-card.tsx +2 -1
  50. package/src/components/chat/chat-header.tsx +8 -13
  51. package/src/components/chat/chat-list.tsx +58 -20
  52. package/src/components/chat/message-list.tsx +142 -18
  53. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  54. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  55. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  56. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +157 -86
  59. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  60. package/src/components/gateways/gateway-sheet.tsx +567 -0
  61. package/src/components/input/chat-input.tsx +135 -86
  62. package/src/components/layout/app-layout.tsx +2 -0
  63. package/src/components/memory/memory-browser.tsx +71 -6
  64. package/src/components/memory/memory-card.tsx +18 -0
  65. package/src/components/memory/memory-detail.tsx +58 -31
  66. package/src/components/memory/memory-sheet.tsx +32 -4
  67. package/src/components/projects/project-detail.tsx +7 -2
  68. package/src/components/providers/provider-list.tsx +158 -2
  69. package/src/components/providers/provider-sheet.tsx +81 -70
  70. package/src/components/shared/bottom-sheet.tsx +31 -15
  71. package/src/components/shared/confirm-dialog.tsx +45 -30
  72. package/src/components/shared/model-combobox.tsx +90 -8
  73. package/src/components/shared/settings/section-heartbeat.tsx +11 -6
  74. package/src/components/shared/settings/section-orchestrator.tsx +3 -0
  75. package/src/components/shared/settings/settings-page.tsx +5 -3
  76. package/src/components/tasks/approvals-panel.tsx +7 -1
  77. package/src/components/ui/dialog.tsx +2 -2
  78. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  79. package/src/lib/heartbeat-defaults.ts +48 -0
  80. package/src/lib/memory-presentation.ts +59 -0
  81. package/src/lib/provider-model-discovery-client.ts +29 -0
  82. package/src/lib/providers/index.ts +12 -5
  83. package/src/lib/runtime-loop.ts +105 -3
  84. package/src/lib/safe-storage.ts +6 -1
  85. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  86. package/src/lib/server/agent-runtime-config.ts +277 -0
  87. package/src/lib/server/approvals-auto-approve.test.ts +59 -0
  88. package/src/lib/server/build-llm.test.ts +13 -5
  89. package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
  90. package/src/lib/server/chat-execution.ts +159 -71
  91. package/src/lib/server/chatroom-helpers.test.ts +7 -0
  92. package/src/lib/server/chatroom-helpers.ts +99 -6
  93. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  94. package/src/lib/server/connectors/manager.ts +89 -61
  95. package/src/lib/server/connectors/slack.ts +1 -1
  96. package/src/lib/server/daemon-state.ts +3 -2
  97. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  98. package/src/lib/server/eval/agent-regression.ts +1742 -0
  99. package/src/lib/server/eval/runner.ts +11 -1
  100. package/src/lib/server/eval/store.ts +2 -1
  101. package/src/lib/server/heartbeat-service.ts +10 -4
  102. package/src/lib/server/main-agent-loop.ts +13 -6
  103. package/src/lib/server/openclaw-exec-config.ts +4 -2
  104. package/src/lib/server/openclaw-gateway.ts +123 -36
  105. package/src/lib/server/orchestrator-lg.ts +1 -2
  106. package/src/lib/server/orchestrator.ts +3 -2
  107. package/src/lib/server/plugins.test.ts +9 -1
  108. package/src/lib/server/plugins.ts +12 -2
  109. package/src/lib/server/provider-model-discovery.ts +481 -0
  110. package/src/lib/server/queue.ts +1 -1
  111. package/src/lib/server/runtime-settings.test.ts +119 -0
  112. package/src/lib/server/runtime-settings.ts +12 -92
  113. package/src/lib/server/schedule-normalization.ts +187 -0
  114. package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
  115. package/src/lib/server/session-tools/crud.ts +27 -3
  116. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  117. package/src/lib/server/session-tools/discovery.ts +18 -8
  118. package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
  119. package/src/lib/server/session-tools/file.ts +8 -2
  120. package/src/lib/server/session-tools/http.ts +9 -3
  121. package/src/lib/server/session-tools/index.ts +31 -1
  122. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  123. package/src/lib/server/session-tools/monitor.ts +14 -7
  124. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  125. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  126. package/src/lib/server/session-tools/platform.ts +1 -1
  127. package/src/lib/server/session-tools/plugin-creator.ts +9 -2
  128. package/src/lib/server/session-tools/sandbox.ts +51 -92
  129. package/src/lib/server/session-tools/session-info.ts +22 -1
  130. package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
  131. package/src/lib/server/session-tools/shell.ts +2 -2
  132. package/src/lib/server/session-tools/subagent.ts +3 -1
  133. package/src/lib/server/session-tools/web.ts +73 -30
  134. package/src/lib/server/storage.ts +29 -3
  135. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  136. package/src/lib/server/stream-agent-chat.ts +139 -4
  137. package/src/lib/server/structured-extract.ts +1 -1
  138. package/src/lib/server/task-mention.ts +0 -1
  139. package/src/lib/server/tool-aliases.ts +37 -6
  140. package/src/lib/server/tool-capability-policy.ts +1 -1
  141. package/src/lib/setup-defaults.ts +352 -11
  142. package/src/lib/tool-definitions.ts +3 -4
  143. package/src/lib/validation/schemas.ts +55 -1
  144. package/src/stores/use-app-store.ts +43 -1
  145. package/src/stores/use-chatroom-store.ts +153 -26
  146. package/src/types/index.ts +189 -6
  147. 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
- <div className="fixed inset-0 z-100 flex items-center justify-center p-6">
18
- <div className="absolute inset-0 bg-black/80 backdrop-blur-sm" onClick={onCancel} />
19
- <div className="relative glass rounded-[20px] p-6 w-full max-w-[380px]
20
- shadow-[0_24px_80px_rgba(0,0,0,0.5)]"
21
- style={{ animation: 'fade-in 0.2s cubic-bezier(0.16, 1, 0.3, 1)' }}>
22
- <h3 className="font-display text-[18px] font-700 tracking-[-0.02em] mb-2">{title}</h3>
23
- <p className="text-[13px] text-text-2 mb-6 leading-relaxed">{message}</p>
24
- <div className="flex gap-2.5">
25
- <button
26
- onClick={onCancel}
27
- className="flex-1 py-2.5 rounded-[12px] border border-white/[0.06] bg-transparent text-text-2 text-[13px] font-600 cursor-pointer
28
- hover:bg-surface transition-all duration-200"
29
- style={{ fontFamily: 'inherit' }}
30
- >
31
- Cancel
32
- </button>
33
- <button
34
- onClick={onConfirm}
35
- className={`flex-1 py-2.5 rounded-[12px] border-none text-[13px] font-600 cursor-pointer active:scale-[0.97] transition-all duration-200
36
- ${danger
37
- ? 'bg-danger text-white shadow-[0_4px_20px_rgba(244,63,94,0.2)]'
38
- : 'bg-accent-bright text-white shadow-[0_4px_20px_rgba(99,102,241,0.2)]'}`}
39
- style={{ fontFamily: 'inherit' }}
40
- >
41
- {confirmLabel}
42
- </button>
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
- </div>
45
- </div>
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 filtered = query
31
- ? models.filter((m) => m.toLowerCase().includes(query.toLowerCase()))
32
- : models
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 = query && !models.some((m) => m.toLowerCase() === query.toLowerCase())
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(query.trim())}
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 &ldquo;{query.trim()}&rdquo;</span>
219
+ <span className="truncate">Add &ldquo;{trimmedQuery}&rdquo;</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 ?? 300}
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) : 300 })
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 ?? false) })}
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 ?? true) })}
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 ?? true)
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 ?? true) ? 'On' : 'Off'}
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 sectionProps = { appSettings, patchSettings, inputClass }
121
- const sections = useMemo<SettingsSectionDef[]>(() => [
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
- ], [credList, sectionProps])
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) => Date.now() - req.updatedAt < 60 * 60 * 1000).length,
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/50",
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="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
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
- <div className="fixed inset-0 z-50 flex items-center justify-center">
39
- <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={onClose} />
40
- <div className="relative w-full max-w-md rounded-[16px] border border-white/[0.08] bg-surface-1 shadow-2xl p-6 space-y-5">
41
- <div className="flex items-center gap-2">
42
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-amber-400">
43
- <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" />
44
- <line x1="12" y1="9" x2="12" y2="13" /><line x1="12" y1="17" x2="12.01" y2="17" />
45
- </svg>
46
- <h3 className="font-display text-[15px] font-600 text-text-1">Transaction Approval</h3>
47
- </div>
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
- <div className="p-4 rounded-[12px] bg-black/20 border border-white/[0.06] space-y-3">
50
- <div className="flex items-center justify-between">
51
- <span className="text-[11px] text-text-3/70 uppercase tracking-wide">Amount</span>
52
- <span className="text-[16px] font-600 text-text-1">{amountSol.toFixed(4)} SOL</span>
53
- </div>
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 uppercase tracking-wide block mb-1">Reason</span>
65
- <p className="text-[12px] text-text-2">{transaction.memo}</p>
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
- </div>
69
-
70
- <p className="text-[11px] text-amber-400/80">
71
- Crypto transactions are irreversible. Verify the recipient address carefully.
72
- </p>
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
- {error && <p className="text-[11px] text-red-400">{error}</p>}
78
+ {error && <p className="text-[11px] text-red-400">{error}</p>}
75
79
 
76
- <div className="flex gap-3">
77
- <button
78
- type="button"
79
- onClick={() => handleDecision('deny')}
80
- disabled={submitting}
81
- className="flex-1 px-4 py-2.5 rounded-[10px] border border-white/[0.08] bg-surface text-text-3 text-[12px] font-600 hover:text-red-400 hover:border-red-400/30 transition-colors cursor-pointer disabled:opacity-50"
82
- style={{ fontFamily: 'inherit' }}
83
- >
84
- Deny
85
- </button>
86
- <button
87
- type="button"
88
- onClick={() => handleDecision('approve')}
89
- disabled={submitting}
90
- className="flex-1 px-4 py-2.5 rounded-[10px] bg-accent text-white text-[12px] font-600 hover:brightness-110 transition-all cursor-pointer disabled:opacity-50"
91
- style={{ fontFamily: 'inherit' }}
92
- >
93
- {submitting ? 'Processing...' : 'Approve & Send'}
94
- </button>
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
- </div>
97
- </div>
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
+ }