@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
@@ -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 justify-center py-2 mb-2">
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-6 py-2.5 rounded-pill border border-danger/20 bg-danger/[0.06]
142
- text-danger text-[13px] font-600 cursor-pointer transition-all duration-200
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 generating
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">Queued</span>
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
- onClick={() => fileInputRef.current?.click()}
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="1.5" strokeLinecap="round">
204
- <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" />
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">Attach</span>
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&apos;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 results = await searchMemory({ q: query || undefined, agentId: apiAgentId })
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 yet</p>
211
- <p className="text-[12px] text-text-3/50">Agents store knowledge here as they learn</p>
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">Assigned to</label>
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">Agent</span>
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
- {/* Delete confirmation */}
548
- {confirmDelete && (
549
- <div className="fixed inset-0 z-50 flex items-center justify-center">
550
- <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setConfirmDelete(false)} />
551
- <div className="relative bg-raised rounded-[16px] p-6 max-w-[360px] w-full shadow-xl border border-white/[0.06]"
552
- style={{ animation: 'fade-in 0.15s cubic-bezier(0.16, 1, 0.3, 1)' }}>
553
- <h3 className="font-display text-[16px] font-700 mb-2">Delete Memory</h3>
554
- <p className="text-[13px] text-text-3 mb-5">
555
- Delete &ldquo;{entry.title}&rdquo;? This cannot be undone.
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
  }