@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
|
@@ -24,9 +24,11 @@ const MAX_FILE_SIZE = 10 * 1024 * 1024 // 10 MB
|
|
|
24
24
|
|
|
25
25
|
export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }: Props) {
|
|
26
26
|
const [value, setValue] = useState('')
|
|
27
|
+
const [extrasOpen, setExtrasOpen] = useState(false)
|
|
27
28
|
const { ref: textareaRef, resize } = useAutoResize()
|
|
28
29
|
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
29
30
|
const imageInputRef = useRef<HTMLInputElement>(null)
|
|
31
|
+
const extrasRef = useRef<HTMLDivElement>(null)
|
|
30
32
|
const pendingFiles = useChatStore((s) => s.pendingFiles)
|
|
31
33
|
const addPendingFile = useChatStore((s) => s.addPendingFile)
|
|
32
34
|
const removePendingFile = useChatStore((s) => s.removePendingFile)
|
|
@@ -37,6 +39,17 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
|
|
|
37
39
|
const addQueuedMessage = useChatStore((s) => s.addQueuedMessage)
|
|
38
40
|
const removeQueuedMessage = useChatStore((s) => s.removeQueuedMessage)
|
|
39
41
|
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!extrasOpen) return
|
|
44
|
+
const handler = (e: MouseEvent) => {
|
|
45
|
+
if (extrasRef.current && !extrasRef.current.contains(e.target as Node)) {
|
|
46
|
+
setExtrasOpen(false)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
document.addEventListener('mousedown', handler)
|
|
50
|
+
return () => document.removeEventListener('mousedown', handler)
|
|
51
|
+
}, [extrasOpen])
|
|
52
|
+
|
|
40
53
|
// Draft persistence: restore on session change
|
|
41
54
|
const draftTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
42
55
|
useEffect(() => {
|
|
@@ -61,6 +74,10 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
|
|
|
61
74
|
if (!text && !pendingFiles.length) return
|
|
62
75
|
// If streaming, queue the message instead of blocking
|
|
63
76
|
if (streaming) {
|
|
77
|
+
if (pendingFiles.length > 0) {
|
|
78
|
+
toast.error('Wait for the current reply to finish before sending files.')
|
|
79
|
+
return
|
|
80
|
+
}
|
|
64
81
|
if (text) {
|
|
65
82
|
addQueuedMessage(text)
|
|
66
83
|
setValue('')
|
|
@@ -133,24 +150,30 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
|
|
|
133
150
|
return (
|
|
134
151
|
<div className="shrink-0 px-4 md:px-12 lg:px-16 pb-4 pt-2 fixed bottom-0 left-0 right-0 z-20 bg-bg/95 backdrop-blur-md md:relative md:z-auto md:bg-transparent md:backdrop-blur-none"
|
|
135
152
|
style={{ paddingBottom: 'max(16px, env(safe-area-inset-bottom))' }}>
|
|
136
|
-
<div>
|
|
153
|
+
<div className="relative" ref={extrasRef}>
|
|
137
154
|
{streaming && (
|
|
138
|
-
<div className="flex
|
|
155
|
+
<div className="mb-2 flex flex-wrap items-center justify-between gap-2 rounded-[14px] border border-amber-500/15 bg-amber-500/[0.06] px-3.5 py-2">
|
|
156
|
+
<div className="min-w-0">
|
|
157
|
+
<div className="text-[12px] font-600 text-amber-300">Reply in progress</div>
|
|
158
|
+
<div className="text-[11px] text-amber-200/70">
|
|
159
|
+
New text sends queue automatically. File uploads wait for the current reply to finish.
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
139
162
|
<button
|
|
140
163
|
onClick={onStop}
|
|
141
|
-
className="px-
|
|
142
|
-
text-danger text-[
|
|
143
|
-
active:scale-95 hover:bg-danger/[0.1] hover:border-danger/30"
|
|
164
|
+
className="px-4 py-2 rounded-pill border border-danger/20 bg-danger/[0.06]
|
|
165
|
+
text-danger text-[12px] font-600 cursor-pointer transition-all duration-200
|
|
166
|
+
active:scale-95 hover:bg-danger/[0.1] hover:border-danger/30 shrink-0"
|
|
144
167
|
style={{ fontFamily: 'inherit' }}
|
|
145
168
|
>
|
|
146
|
-
Stop
|
|
169
|
+
Stop
|
|
147
170
|
</button>
|
|
148
171
|
</div>
|
|
149
172
|
)}
|
|
150
173
|
|
|
151
174
|
{queuedMessages.length > 0 && (
|
|
152
175
|
<div className="flex flex-wrap items-center gap-1.5 mb-2">
|
|
153
|
-
<span className="label-mono text-amber-400/70">
|
|
176
|
+
<span className="label-mono text-amber-400/70">Sending next</span>
|
|
154
177
|
{queuedMessages.map((msg, i) => (
|
|
155
178
|
<span key={i} className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-amber-500/10 border border-amber-500/15 text-[12px] text-amber-300 font-mono max-w-[200px]">
|
|
156
179
|
<span className="truncate">{msg}</span>
|
|
@@ -195,92 +218,19 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
|
|
|
195
218
|
|
|
196
219
|
<div className="flex items-center gap-1 px-4 pb-3.5">
|
|
197
220
|
<button
|
|
198
|
-
|
|
221
|
+
type="button"
|
|
222
|
+
onClick={() => setExtrasOpen((open) => !open)}
|
|
199
223
|
className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
|
|
200
224
|
text-text-3 text-[13px] cursor-pointer hover:text-text-2 hover:bg-white/[0.05] transition-all duration-200"
|
|
201
225
|
style={{ fontFamily: 'inherit' }}
|
|
202
226
|
>
|
|
203
|
-
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="
|
|
204
|
-
<path d="
|
|
227
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
228
|
+
<path d="M12 5v14" />
|
|
229
|
+
<path d="M5 12h14" />
|
|
205
230
|
</svg>
|
|
206
|
-
<span className="hidden sm:inline">
|
|
231
|
+
<span className="hidden sm:inline">Add</span>
|
|
207
232
|
</button>
|
|
208
233
|
|
|
209
|
-
<button
|
|
210
|
-
onClick={() => imageInputRef.current?.click()}
|
|
211
|
-
className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
|
|
212
|
-
text-text-3 text-[13px] cursor-pointer hover:text-text-2 hover:bg-white/[0.05] transition-all duration-200"
|
|
213
|
-
style={{ fontFamily: 'inherit' }}
|
|
214
|
-
>
|
|
215
|
-
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
216
|
-
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
217
|
-
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
218
|
-
<polyline points="21 15 16 10 5 21" />
|
|
219
|
-
</svg>
|
|
220
|
-
<span className="hidden sm:inline">Image</span>
|
|
221
|
-
</button>
|
|
222
|
-
|
|
223
|
-
{/* Plugin Chat Actions */}
|
|
224
|
-
{pluginChatActions.map((action) => (
|
|
225
|
-
<Tooltip key={action.id}>
|
|
226
|
-
<TooltipTrigger asChild>
|
|
227
|
-
<button
|
|
228
|
-
onClick={() => {
|
|
229
|
-
if (action.action === 'message') onSend(action.value)
|
|
230
|
-
else if (action.action === 'link') window.open(action.value, '_blank')
|
|
231
|
-
}}
|
|
232
|
-
className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-emerald-500/[0.05]
|
|
233
|
-
text-emerald-400 text-[13px] cursor-pointer hover:text-emerald-300 hover:bg-emerald-500/[0.1] transition-all duration-200"
|
|
234
|
-
style={{ fontFamily: 'inherit' }}
|
|
235
|
-
>
|
|
236
|
-
{action.label}
|
|
237
|
-
</button>
|
|
238
|
-
</TooltipTrigger>
|
|
239
|
-
{action.tooltip && <TooltipContent>{action.tooltip}</TooltipContent>}
|
|
240
|
-
</Tooltip>
|
|
241
|
-
))}
|
|
242
|
-
|
|
243
|
-
{micSupported && (
|
|
244
|
-
<button
|
|
245
|
-
onClick={toggleRecording}
|
|
246
|
-
className={`flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
|
|
247
|
-
text-[13px] cursor-pointer transition-all duration-200
|
|
248
|
-
${recording ? 'text-danger' : 'text-text-3 hover:text-text-2 hover:bg-white/[0.05]'}`}
|
|
249
|
-
style={recording ? { animation: 'mic-pulse 1.5s ease-out infinite', fontFamily: 'inherit' } : { fontFamily: 'inherit' }}
|
|
250
|
-
>
|
|
251
|
-
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
252
|
-
<rect x="9" y="2" width="6" height="11" rx="3" />
|
|
253
|
-
<path d="M5 10a7 7 0 0 0 14 0" />
|
|
254
|
-
<line x1="12" y1="19" x2="12" y2="22" />
|
|
255
|
-
</svg>
|
|
256
|
-
</button>
|
|
257
|
-
)}
|
|
258
|
-
|
|
259
|
-
<Tooltip>
|
|
260
|
-
<TooltipTrigger asChild>
|
|
261
|
-
<button
|
|
262
|
-
type="button"
|
|
263
|
-
onClick={() => { useChatStore.getState().clearContext() }}
|
|
264
|
-
disabled={streaming}
|
|
265
|
-
className="flex items-center gap-1.5 px-3 py-2 rounded-[10px] border-none bg-transparent
|
|
266
|
-
text-text-3 text-[13px] cursor-pointer hover:text-amber-400 hover:bg-amber-400/10 transition-all duration-200 disabled:opacity-30 disabled:pointer-events-none"
|
|
267
|
-
style={{ fontFamily: 'inherit' }}
|
|
268
|
-
>
|
|
269
|
-
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
270
|
-
<line x1="2" y1="12" x2="22" y2="12" />
|
|
271
|
-
<polyline points="8 8 4 12 8 16" />
|
|
272
|
-
<polyline points="16 8 20 12 16 16" />
|
|
273
|
-
</svg>
|
|
274
|
-
<span className="hidden sm:inline">New context</span>
|
|
275
|
-
</button>
|
|
276
|
-
</TooltipTrigger>
|
|
277
|
-
<TooltipContent side="top" sideOffset={8}
|
|
278
|
-
className="bg-raised border border-white/[0.08] text-text shadow-[0_8px_32px_rgba(0,0,0,0.5)] rounded-[10px] px-3.5 py-2.5 max-w-[220px]">
|
|
279
|
-
<div className="font-display text-[12px] font-600 mb-0.5">New context window</div>
|
|
280
|
-
<div className="text-[11px] text-text-3 leading-[1.4]">Adds a marker — messages above it won't be sent to the AI. Nothing is deleted.</div>
|
|
281
|
-
</TooltipContent>
|
|
282
|
-
</Tooltip>
|
|
283
|
-
|
|
284
234
|
<div className="flex-1" />
|
|
285
235
|
|
|
286
236
|
<span className="text-[11px] text-text-3/60 tabular-nums mr-2 font-mono">
|
|
@@ -314,6 +264,105 @@ export function ChatInput({ streaming, onSend, onStop, pluginChatActions = [] }:
|
|
|
314
264
|
</div>
|
|
315
265
|
</div>
|
|
316
266
|
|
|
267
|
+
{extrasOpen && (
|
|
268
|
+
<div className="absolute left-0 bottom-[72px] w-[280px] max-w-[calc(100vw-2rem)] rounded-[16px] border border-white/[0.08] bg-raised/95 p-2 shadow-[0_18px_64px_rgba(0,0,0,0.55)] backdrop-blur-xl">
|
|
269
|
+
<button
|
|
270
|
+
type="button"
|
|
271
|
+
onClick={() => {
|
|
272
|
+
setExtrasOpen(false)
|
|
273
|
+
fileInputRef.current?.click()
|
|
274
|
+
}}
|
|
275
|
+
className="flex w-full items-center gap-2 rounded-[10px] px-3 py-2 text-left text-[13px] text-text-2 hover:bg-white/[0.05] cursor-pointer transition-colors"
|
|
276
|
+
style={{ fontFamily: 'inherit' }}
|
|
277
|
+
>
|
|
278
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
279
|
+
<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />
|
|
280
|
+
</svg>
|
|
281
|
+
Attach files
|
|
282
|
+
</button>
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
onClick={() => {
|
|
286
|
+
setExtrasOpen(false)
|
|
287
|
+
imageInputRef.current?.click()
|
|
288
|
+
}}
|
|
289
|
+
className="flex w-full items-center gap-2 rounded-[10px] px-3 py-2 text-left text-[13px] text-text-2 hover:bg-white/[0.05] cursor-pointer transition-colors"
|
|
290
|
+
style={{ fontFamily: 'inherit' }}
|
|
291
|
+
>
|
|
292
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
293
|
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
|
294
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
295
|
+
<polyline points="21 15 16 10 5 21" />
|
|
296
|
+
</svg>
|
|
297
|
+
Add image
|
|
298
|
+
</button>
|
|
299
|
+
{micSupported && (
|
|
300
|
+
<button
|
|
301
|
+
type="button"
|
|
302
|
+
onClick={() => {
|
|
303
|
+
setExtrasOpen(false)
|
|
304
|
+
toggleRecording()
|
|
305
|
+
}}
|
|
306
|
+
className={`flex w-full items-center gap-2 rounded-[10px] px-3 py-2 text-left text-[13px] cursor-pointer transition-colors ${
|
|
307
|
+
recording ? 'text-danger bg-danger/[0.06]' : 'text-text-2 hover:bg-white/[0.05]'
|
|
308
|
+
}`}
|
|
309
|
+
style={recording ? { animation: 'mic-pulse 1.5s ease-out infinite', fontFamily: 'inherit' } : { fontFamily: 'inherit' }}
|
|
310
|
+
>
|
|
311
|
+
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
312
|
+
<rect x="9" y="2" width="6" height="11" rx="3" />
|
|
313
|
+
<path d="M5 10a7 7 0 0 0 14 0" />
|
|
314
|
+
<line x1="12" y1="19" x2="12" y2="22" />
|
|
315
|
+
</svg>
|
|
316
|
+
{recording ? 'Stop microphone' : 'Use microphone'}
|
|
317
|
+
</button>
|
|
318
|
+
)}
|
|
319
|
+
<button
|
|
320
|
+
type="button"
|
|
321
|
+
onClick={() => {
|
|
322
|
+
setExtrasOpen(false)
|
|
323
|
+
void useChatStore.getState().clearContext()
|
|
324
|
+
}}
|
|
325
|
+
disabled={streaming}
|
|
326
|
+
className="flex w-full items-center gap-2 rounded-[10px] px-3 py-2 text-left text-[13px] text-text-2 hover:bg-white/[0.05] cursor-pointer transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
|
327
|
+
style={{ fontFamily: 'inherit' }}
|
|
328
|
+
>
|
|
329
|
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
|
330
|
+
<line x1="2" y1="12" x2="22" y2="12" />
|
|
331
|
+
<polyline points="8 8 4 12 8 16" />
|
|
332
|
+
<polyline points="16 8 20 12 16 16" />
|
|
333
|
+
</svg>
|
|
334
|
+
New context window
|
|
335
|
+
</button>
|
|
336
|
+
{pluginChatActions.length > 0 && (
|
|
337
|
+
<>
|
|
338
|
+
<div className="mx-2 my-1 h-px bg-white/[0.06]" />
|
|
339
|
+
<div className="px-3 pb-1 pt-1 text-[10px] font-700 uppercase tracking-[0.08em] text-text-3/50">
|
|
340
|
+
Quick actions
|
|
341
|
+
</div>
|
|
342
|
+
{pluginChatActions.map((action) => (
|
|
343
|
+
<Tooltip key={action.id}>
|
|
344
|
+
<TooltipTrigger asChild>
|
|
345
|
+
<button
|
|
346
|
+
type="button"
|
|
347
|
+
onClick={() => {
|
|
348
|
+
setExtrasOpen(false)
|
|
349
|
+
if (action.action === 'message') onSend(action.value)
|
|
350
|
+
else if (action.action === 'link') window.open(action.value, '_blank')
|
|
351
|
+
}}
|
|
352
|
+
className="flex w-full items-center gap-2 rounded-[10px] px-3 py-2 text-left text-[13px] text-emerald-300 hover:bg-emerald-500/[0.08] cursor-pointer transition-colors"
|
|
353
|
+
style={{ fontFamily: 'inherit' }}
|
|
354
|
+
>
|
|
355
|
+
{action.label}
|
|
356
|
+
</button>
|
|
357
|
+
</TooltipTrigger>
|
|
358
|
+
{action.tooltip && <TooltipContent>{action.tooltip}</TooltipContent>}
|
|
359
|
+
</Tooltip>
|
|
360
|
+
))}
|
|
361
|
+
</>
|
|
362
|
+
)}
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
|
|
317
366
|
<input
|
|
318
367
|
ref={fileInputRef}
|
|
319
368
|
type="file"
|
|
@@ -24,6 +24,7 @@ import { SecretsList } from '@/components/secrets/secrets-list'
|
|
|
24
24
|
import { SecretSheet } from '@/components/secrets/secret-sheet'
|
|
25
25
|
import { ProviderList } from '@/components/providers/provider-list'
|
|
26
26
|
import { ProviderSheet } from '@/components/providers/provider-sheet'
|
|
27
|
+
import { GatewaySheet } from '@/components/gateways/gateway-sheet'
|
|
27
28
|
import { SkillList } from '@/components/skills/skill-list'
|
|
28
29
|
import { SkillSheet } from '@/components/skills/skill-sheet'
|
|
29
30
|
import { ConnectorList } from '@/components/connectors/connector-list'
|
|
@@ -1097,6 +1098,7 @@ export function AppLayout() {
|
|
|
1097
1098
|
<TaskSheet />
|
|
1098
1099
|
<SecretSheet />
|
|
1099
1100
|
<ProviderSheet />
|
|
1101
|
+
<GatewaySheet />
|
|
1100
1102
|
<SkillSheet />
|
|
1101
1103
|
<ConnectorSheet />
|
|
1102
1104
|
<ChatroomSheet />
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
4
4
|
import { searchMemory } from '@/lib/memory'
|
|
5
|
+
import { deriveMemoryScope, getMemoryTier } from '@/lib/memory-presentation'
|
|
5
6
|
import { useAppStore } from '@/stores/use-app-store'
|
|
6
7
|
import { MemoryCard } from './memory-card'
|
|
7
8
|
import { MemoryDetail } from './memory-detail'
|
|
@@ -14,6 +15,10 @@ export function MemoryBrowser() {
|
|
|
14
15
|
const refreshKey = useAppStore((s) => s.memoryRefreshKey)
|
|
15
16
|
const agents = useAppStore((s) => s.agents)
|
|
16
17
|
const memoryAgentFilter = useAppStore((s) => s.memoryAgentFilter)
|
|
18
|
+
const memoryTierFilter = useAppStore((s) => s.memoryTierFilter)
|
|
19
|
+
const setMemoryTierFilter = useAppStore((s) => s.setMemoryTierFilter)
|
|
20
|
+
const memoryScopeFilter = useAppStore((s) => s.memoryScopeFilter)
|
|
21
|
+
const setMemoryScopeFilter = useAppStore((s) => s.setMemoryScopeFilter)
|
|
17
22
|
|
|
18
23
|
const [search, setSearch] = useState('')
|
|
19
24
|
const [entries, setEntries] = useState<MemoryEntry[]>([])
|
|
@@ -32,14 +37,24 @@ export function MemoryBrowser() {
|
|
|
32
37
|
|
|
33
38
|
const load = useCallback(async (query: string) => {
|
|
34
39
|
try {
|
|
35
|
-
const
|
|
40
|
+
const scope = memoryAgentFilter === '_global'
|
|
41
|
+
? 'global'
|
|
42
|
+
: memoryAgentFilter
|
|
43
|
+
? 'auto'
|
|
44
|
+
: 'all'
|
|
45
|
+
const results = await searchMemory({
|
|
46
|
+
q: query || undefined,
|
|
47
|
+
agentId: apiAgentId,
|
|
48
|
+
scope,
|
|
49
|
+
limit: 120,
|
|
50
|
+
})
|
|
36
51
|
setEntries(Array.isArray(results) ? results : [])
|
|
37
52
|
setError(null)
|
|
38
53
|
} catch {
|
|
39
54
|
setError('Unable to load memories right now.')
|
|
40
55
|
}
|
|
41
56
|
setLoaded(true)
|
|
42
|
-
}, [apiAgentId])
|
|
57
|
+
}, [apiAgentId, memoryAgentFilter])
|
|
43
58
|
|
|
44
59
|
useEffect(() => {
|
|
45
60
|
searchRef.current = search
|
|
@@ -70,12 +85,23 @@ export function MemoryBrowser() {
|
|
|
70
85
|
|
|
71
86
|
const filtered = useMemo(() => {
|
|
72
87
|
return entries.filter((e) => {
|
|
73
|
-
// Client-side global filter
|
|
74
88
|
if (memoryAgentFilter === '_global' && e.agentId) return false
|
|
89
|
+
if (memoryAgentFilter && memoryAgentFilter !== '_global') {
|
|
90
|
+
const visibleToAgent = e.agentId === memoryAgentFilter || (Array.isArray(e.sharedWith) && e.sharedWith.includes(memoryAgentFilter)) || !e.agentId
|
|
91
|
+
if (!visibleToAgent) return false
|
|
92
|
+
}
|
|
93
|
+
const scope = deriveMemoryScope(e)
|
|
94
|
+
if (memoryScopeFilter !== 'all') {
|
|
95
|
+
if (memoryScopeFilter === 'global' && scope !== 'global') return false
|
|
96
|
+
if (memoryScopeFilter === 'agent' && scope !== 'agent' && scope !== 'shared') return false
|
|
97
|
+
if (memoryScopeFilter === 'session' && scope !== 'session') return false
|
|
98
|
+
if (memoryScopeFilter === 'project' && scope !== 'project') return false
|
|
99
|
+
}
|
|
100
|
+
if (memoryTierFilter !== 'all' && getMemoryTier(e) !== memoryTierFilter) return false
|
|
75
101
|
if (categoryFilter && (e.category || 'note') !== categoryFilter) return false
|
|
76
102
|
return true
|
|
77
103
|
})
|
|
78
|
-
}, [entries, memoryAgentFilter, categoryFilter])
|
|
104
|
+
}, [entries, memoryAgentFilter, memoryScopeFilter, memoryTierFilter, categoryFilter])
|
|
79
105
|
|
|
80
106
|
const filterLabel = useMemo(() => {
|
|
81
107
|
if (!memoryAgentFilter) return 'All Memories'
|
|
@@ -135,6 +161,41 @@ export function MemoryBrowser() {
|
|
|
135
161
|
text-[13px] outline-none transition-all duration-200 placeholder:text-text-3/70 focus-glow"
|
|
136
162
|
style={{ fontFamily: 'inherit' }}
|
|
137
163
|
/>
|
|
164
|
+
<div className="mt-2.5 flex flex-wrap items-center gap-1.5">
|
|
165
|
+
{(['all', 'global', 'agent', 'session', 'project'] as const).map((scope) => (
|
|
166
|
+
<button
|
|
167
|
+
key={scope}
|
|
168
|
+
type="button"
|
|
169
|
+
onClick={() => setMemoryScopeFilter(scope)}
|
|
170
|
+
className={`px-2.5 py-1 rounded-[8px] text-[10px] font-700 uppercase tracking-[0.08em] border transition-all ${
|
|
171
|
+
memoryScopeFilter === scope
|
|
172
|
+
? 'bg-accent-soft text-accent-bright border-accent-bright/15'
|
|
173
|
+
: 'bg-transparent text-text-3/70 border-white/[0.05] hover:text-text-2 hover:bg-white/[0.03]'
|
|
174
|
+
}`}
|
|
175
|
+
>
|
|
176
|
+
{scope === 'agent' ? 'private/shared' : scope}
|
|
177
|
+
</button>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
<div className="mt-1.5 flex flex-wrap items-center gap-1.5">
|
|
181
|
+
{(['all', 'working', 'durable', 'archive'] as const).map((tier) => (
|
|
182
|
+
<button
|
|
183
|
+
key={tier}
|
|
184
|
+
type="button"
|
|
185
|
+
onClick={() => setMemoryTierFilter(tier)}
|
|
186
|
+
className={`px-2.5 py-1 rounded-[8px] text-[10px] font-700 uppercase tracking-[0.08em] border transition-all ${
|
|
187
|
+
memoryTierFilter === tier
|
|
188
|
+
? 'bg-white/[0.08] text-text-2 border-white/[0.10]'
|
|
189
|
+
: 'bg-transparent text-text-3/70 border-white/[0.05] hover:text-text-2 hover:bg-white/[0.03]'
|
|
190
|
+
}`}
|
|
191
|
+
>
|
|
192
|
+
{tier}
|
|
193
|
+
</button>
|
|
194
|
+
))}
|
|
195
|
+
</div>
|
|
196
|
+
<p className="mt-2 text-[11px] text-text-3/55">
|
|
197
|
+
Scope shows what kind of memory it is. Tier shows how long it should stay salient.
|
|
198
|
+
</p>
|
|
138
199
|
</div>
|
|
139
200
|
|
|
140
201
|
{/* Category chips */}
|
|
@@ -207,8 +268,12 @@ export function MemoryBrowser() {
|
|
|
207
268
|
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
|
|
208
269
|
</svg>
|
|
209
270
|
</div>
|
|
210
|
-
<p className="font-display text-[14px] font-600 text-text-2">No memories
|
|
211
|
-
<p className="text-[12px] text-text-3/50">
|
|
271
|
+
<p className="font-display text-[14px] font-600 text-text-2">No memories match these filters</p>
|
|
272
|
+
<p className="text-[12px] text-text-3/50">
|
|
273
|
+
{memoryScopeFilter === 'all' && memoryTierFilter === 'all'
|
|
274
|
+
? 'Agents store knowledge here as they learn'
|
|
275
|
+
: `Try a different ${memoryScopeFilter !== 'all' ? 'scope' : 'tier'} filter`}
|
|
276
|
+
</p>
|
|
212
277
|
</div>
|
|
213
278
|
) : null
|
|
214
279
|
) : (
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import type { MemoryEntry } from '@/types'
|
|
4
4
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
5
|
+
import { deriveMemoryScope, getMemoryScopeLabel, getMemoryTier } from '@/lib/memory-presentation'
|
|
5
6
|
|
|
6
7
|
function timeAgo(ts: number): string {
|
|
7
8
|
if (!ts) return ''
|
|
@@ -22,6 +23,9 @@ interface Props {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export function MemoryCard({ entry, active, agentName, agentAvatarSeed, agentAvatarUrl, onClick }: Props) {
|
|
26
|
+
const scope = deriveMemoryScope(entry)
|
|
27
|
+
const tier = getMemoryTier(entry)
|
|
28
|
+
|
|
25
29
|
return (
|
|
26
30
|
<div
|
|
27
31
|
onClick={onClick}
|
|
@@ -51,6 +55,20 @@ export function MemoryCard({ entry, active, agentName, agentAvatarSeed, agentAva
|
|
|
51
55
|
<div className="text-[12px] text-text-2/40 mt-1 line-clamp-3 leading-relaxed">
|
|
52
56
|
{entry.content || '(empty)'}
|
|
53
57
|
</div>
|
|
58
|
+
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
|
59
|
+
<span className="px-1.5 py-0.5 rounded-[5px] text-[9px] font-700 uppercase tracking-[0.08em] bg-white/[0.04] text-text-3/75">
|
|
60
|
+
{getMemoryScopeLabel(scope)}
|
|
61
|
+
</span>
|
|
62
|
+
<span className={`px-1.5 py-0.5 rounded-[5px] text-[9px] font-700 uppercase tracking-[0.08em] ${
|
|
63
|
+
tier === 'working'
|
|
64
|
+
? 'bg-amber-400/10 text-amber-300'
|
|
65
|
+
: tier === 'archive'
|
|
66
|
+
? 'bg-sky-400/10 text-sky-300'
|
|
67
|
+
: 'bg-emerald-400/10 text-emerald-300'
|
|
68
|
+
}`}>
|
|
69
|
+
{tier}
|
|
70
|
+
</span>
|
|
71
|
+
</div>
|
|
54
72
|
{(entry.image?.path || entry.imagePath) && (
|
|
55
73
|
<div className="mt-2 w-10 h-10 rounded-[6px] overflow-hidden bg-white/[0.04] shrink-0">
|
|
56
74
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import { useEffect, useState, useCallback } from 'react'
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { getMemory, updateMemory, deleteMemory } from '@/lib/memory'
|
|
6
|
+
import { deriveMemoryScope, getMemoryScopeLabel, getMemoryTier } from '@/lib/memory-presentation'
|
|
6
7
|
import { AgentAvatar } from '@/components/agents/agent-avatar'
|
|
8
|
+
import { ConfirmDialog } from '@/components/shared/confirm-dialog'
|
|
7
9
|
import type { MemoryEntry } from '@/types'
|
|
8
10
|
|
|
9
11
|
const CATEGORIES = ['note', 'fact', 'preference', 'finding', 'learning', 'general']
|
|
@@ -22,6 +24,7 @@ export function MemoryDetail() {
|
|
|
22
24
|
const [title, setTitle] = useState('')
|
|
23
25
|
const [content, setContent] = useState('')
|
|
24
26
|
const [category, setCategory] = useState('note')
|
|
27
|
+
const [editTier, setEditTier] = useState<'working' | 'durable' | 'archive'>('durable')
|
|
25
28
|
const [editAgentId, setEditAgentId] = useState<string | null>(null)
|
|
26
29
|
const [editSharedWith, setEditSharedWith] = useState<string[]>([])
|
|
27
30
|
const [saving, setSaving] = useState(false)
|
|
@@ -53,6 +56,7 @@ export function MemoryDetail() {
|
|
|
53
56
|
setTitle(resolved.title)
|
|
54
57
|
setContent(resolved.content)
|
|
55
58
|
setCategory(resolved.category || 'note')
|
|
59
|
+
setEditTier(getMemoryTier(resolved))
|
|
56
60
|
setEditAgentId(resolved.agentId || null)
|
|
57
61
|
setEditSharedWith(resolved.sharedWith || [])
|
|
58
62
|
setEditing(false)
|
|
@@ -97,6 +101,12 @@ export function MemoryDetail() {
|
|
|
97
101
|
category,
|
|
98
102
|
agentId: editAgentId,
|
|
99
103
|
sharedWith: editSharedWith.length ? editSharedWith : undefined,
|
|
104
|
+
metadata: {
|
|
105
|
+
...(entry.metadata || {}),
|
|
106
|
+
tier: editTier,
|
|
107
|
+
scope: editAgentId ? 'agent' : 'global',
|
|
108
|
+
visibility: editAgentId ? (editSharedWith.length ? 'shared' : 'private') : 'global',
|
|
109
|
+
},
|
|
100
110
|
})
|
|
101
111
|
setEntry(updated)
|
|
102
112
|
setEditing(false)
|
|
@@ -151,6 +161,8 @@ export function MemoryDetail() {
|
|
|
151
161
|
|
|
152
162
|
const agentName = entry.agentId ? (agents[entry.agentId]?.name || entry.agentId) : null
|
|
153
163
|
const sessionName = entry.sessionId ? (sessions[entry.sessionId]?.name || entry.sessionId) : null
|
|
164
|
+
const scope = deriveMemoryScope(entry)
|
|
165
|
+
const tier = getMemoryTier(entry)
|
|
154
166
|
const imagePath = entry.image?.path || entry.imagePath || null
|
|
155
167
|
const imageUrl = imagePath
|
|
156
168
|
? imagePath.startsWith('data/memory-images/')
|
|
@@ -171,6 +183,18 @@ export function MemoryDetail() {
|
|
|
171
183
|
<span className="shrink-0 text-[10px] font-700 uppercase tracking-wider text-accent-bright/70 bg-accent-soft px-2 py-0.5 rounded-[6px]">
|
|
172
184
|
{entry.category || 'note'}
|
|
173
185
|
</span>
|
|
186
|
+
<span className="shrink-0 text-[10px] font-700 uppercase tracking-wider text-text-3/70 bg-white/[0.04] px-2 py-0.5 rounded-[6px]">
|
|
187
|
+
{getMemoryScopeLabel(scope)}
|
|
188
|
+
</span>
|
|
189
|
+
<span className={`shrink-0 text-[10px] font-700 uppercase tracking-wider px-2 py-0.5 rounded-[6px] ${
|
|
190
|
+
tier === 'working'
|
|
191
|
+
? 'bg-amber-400/10 text-amber-300'
|
|
192
|
+
: tier === 'archive'
|
|
193
|
+
? 'bg-sky-400/10 text-sky-300'
|
|
194
|
+
: 'bg-emerald-400/10 text-emerald-300'
|
|
195
|
+
}`}>
|
|
196
|
+
{tier}
|
|
197
|
+
</span>
|
|
174
198
|
{!editing && (
|
|
175
199
|
<h2 className="font-display text-[16px] font-700 truncate tracking-[-0.02em]">{entry.title || 'Untitled'}</h2>
|
|
176
200
|
)}
|
|
@@ -216,6 +240,7 @@ export function MemoryDetail() {
|
|
|
216
240
|
setTitle(entry.title)
|
|
217
241
|
setContent(entry.content)
|
|
218
242
|
setCategory(entry.category || 'note')
|
|
243
|
+
setEditTier(getMemoryTier(entry))
|
|
219
244
|
setEditAgentId(entry.agentId || null)
|
|
220
245
|
setEditSharedWith(entry.sharedWith || [])
|
|
221
246
|
setEditing(false)
|
|
@@ -301,9 +326,23 @@ export function MemoryDetail() {
|
|
|
301
326
|
</div>
|
|
302
327
|
</div>
|
|
303
328
|
|
|
329
|
+
<div>
|
|
330
|
+
<label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Tier</label>
|
|
331
|
+
<select
|
|
332
|
+
value={editTier}
|
|
333
|
+
onChange={(e) => setEditTier(e.target.value as typeof editTier)}
|
|
334
|
+
className={inputClass}
|
|
335
|
+
style={{ fontFamily: 'inherit' }}
|
|
336
|
+
>
|
|
337
|
+
<option value="working">Working</option>
|
|
338
|
+
<option value="durable">Durable</option>
|
|
339
|
+
<option value="archive">Archive</option>
|
|
340
|
+
</select>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
304
343
|
{/* Agent assignment */}
|
|
305
344
|
<div>
|
|
306
|
-
<label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">
|
|
345
|
+
<label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Visibility</label>
|
|
307
346
|
<div className="flex gap-1.5 flex-wrap">
|
|
308
347
|
<button
|
|
309
348
|
onClick={() => setEditAgentId(null)}
|
|
@@ -522,10 +561,18 @@ export function MemoryDetail() {
|
|
|
522
561
|
</div>
|
|
523
562
|
{entry.agentId && (
|
|
524
563
|
<div>
|
|
525
|
-
<span className="text-text-3/70 block mb-1">
|
|
564
|
+
<span className="text-text-3/70 block mb-1">Owner</span>
|
|
526
565
|
<span className="text-text-3/60 font-mono">{agentName}</span>
|
|
527
566
|
</div>
|
|
528
567
|
)}
|
|
568
|
+
<div>
|
|
569
|
+
<span className="text-text-3/70 block mb-1">Scope</span>
|
|
570
|
+
<span className="text-text-3/60 font-mono">{getMemoryScopeLabel(scope)}</span>
|
|
571
|
+
</div>
|
|
572
|
+
<div>
|
|
573
|
+
<span className="text-text-3/70 block mb-1">Tier</span>
|
|
574
|
+
<span className="text-text-3/60 font-mono">{tier}</span>
|
|
575
|
+
</div>
|
|
529
576
|
{entry.sessionId && (
|
|
530
577
|
<div>
|
|
531
578
|
<span className="text-text-3/70 block mb-1">Chat</span>
|
|
@@ -544,35 +591,15 @@ export function MemoryDetail() {
|
|
|
544
591
|
</div>
|
|
545
592
|
</div>
|
|
546
593
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
</p>
|
|
557
|
-
<div className="flex gap-3">
|
|
558
|
-
<button
|
|
559
|
-
onClick={() => setConfirmDelete(false)}
|
|
560
|
-
className="flex-1 py-2.5 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[13px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
|
|
561
|
-
style={{ fontFamily: 'inherit' }}
|
|
562
|
-
>
|
|
563
|
-
Cancel
|
|
564
|
-
</button>
|
|
565
|
-
<button
|
|
566
|
-
onClick={handleDelete}
|
|
567
|
-
className="flex-1 py-2.5 rounded-[10px] border-none bg-red-500/90 text-white text-[13px] font-600 cursor-pointer active:scale-[0.97] transition-all hover:bg-red-500"
|
|
568
|
-
style={{ fontFamily: 'inherit' }}
|
|
569
|
-
>
|
|
570
|
-
Delete
|
|
571
|
-
</button>
|
|
572
|
-
</div>
|
|
573
|
-
</div>
|
|
574
|
-
</div>
|
|
575
|
-
)}
|
|
594
|
+
<ConfirmDialog
|
|
595
|
+
open={confirmDelete}
|
|
596
|
+
title="Delete Memory"
|
|
597
|
+
message={`Delete "${entry.title}"? This cannot be undone.`}
|
|
598
|
+
confirmLabel="Delete"
|
|
599
|
+
danger
|
|
600
|
+
onCancel={() => setConfirmDelete(false)}
|
|
601
|
+
onConfirm={() => { void handleDelete() }}
|
|
602
|
+
/>
|
|
576
603
|
</div>
|
|
577
604
|
)
|
|
578
605
|
}
|