@swarmclawai/swarmclaw 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (319) hide show
  1. package/README.md +577 -0
  2. package/bin/server-cmd.js +359 -0
  3. package/bin/swarmclaw.js +29 -0
  4. package/bin/swarmclaw.mjs +1504 -0
  5. package/next.config.ts +33 -0
  6. package/package.json +112 -0
  7. package/postcss.config.mjs +7 -0
  8. package/public/branding/swarmclaw-org-avatar.png +0 -0
  9. package/public/branding/swarmclaw-org-avatar.svg +58 -0
  10. package/public/file.svg +1 -0
  11. package/public/globe.svg +1 -0
  12. package/public/next.svg +1 -0
  13. package/public/screenshots/agents.png +0 -0
  14. package/public/screenshots/connectors.png +0 -0
  15. package/public/screenshots/dashboard.png +0 -0
  16. package/public/screenshots/new-session-openclaw.png +0 -0
  17. package/public/screenshots/providers.png +0 -0
  18. package/public/screenshots/schedules.png +0 -0
  19. package/public/screenshots/tasks.png +0 -0
  20. package/public/vercel.svg +1 -0
  21. package/public/window.svg +1 -0
  22. package/src/app/api/agents/[id]/route.ts +30 -0
  23. package/src/app/api/agents/[id]/thread/route.ts +66 -0
  24. package/src/app/api/agents/generate/route.ts +42 -0
  25. package/src/app/api/agents/route.ts +33 -0
  26. package/src/app/api/auth/route.ts +25 -0
  27. package/src/app/api/claude-skills/route.ts +42 -0
  28. package/src/app/api/clawhub/install/route.ts +39 -0
  29. package/src/app/api/clawhub/search/route.ts +11 -0
  30. package/src/app/api/connectors/[id]/route.ts +79 -0
  31. package/src/app/api/connectors/route.ts +60 -0
  32. package/src/app/api/credentials/[id]/route.ts +14 -0
  33. package/src/app/api/credentials/route.ts +31 -0
  34. package/src/app/api/daemon/health-check/route.ts +11 -0
  35. package/src/app/api/daemon/route.ts +22 -0
  36. package/src/app/api/dirs/pick/route.ts +60 -0
  37. package/src/app/api/dirs/route.ts +29 -0
  38. package/src/app/api/documents/[id]/route.ts +47 -0
  39. package/src/app/api/documents/route.ts +93 -0
  40. package/src/app/api/files/serve/route.ts +69 -0
  41. package/src/app/api/generate/info/route.ts +12 -0
  42. package/src/app/api/generate/route.ts +106 -0
  43. package/src/app/api/ip/route.ts +6 -0
  44. package/src/app/api/knowledge/[id]/route.ts +61 -0
  45. package/src/app/api/knowledge/route.ts +48 -0
  46. package/src/app/api/knowledge/upload/route.ts +86 -0
  47. package/src/app/api/logs/route.ts +65 -0
  48. package/src/app/api/mcp-servers/[id]/route.ts +32 -0
  49. package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
  50. package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
  51. package/src/app/api/mcp-servers/route.ts +27 -0
  52. package/src/app/api/memory/[id]/route.ts +126 -0
  53. package/src/app/api/memory/maintenance/route.ts +63 -0
  54. package/src/app/api/memory/route.ts +111 -0
  55. package/src/app/api/memory-images/[filename]/route.ts +36 -0
  56. package/src/app/api/orchestrator/run/route.ts +43 -0
  57. package/src/app/api/plugins/install/route.ts +58 -0
  58. package/src/app/api/plugins/marketplace/route.ts +33 -0
  59. package/src/app/api/plugins/route.ts +21 -0
  60. package/src/app/api/preview-server/route.ts +339 -0
  61. package/src/app/api/providers/[id]/models/route.ts +29 -0
  62. package/src/app/api/providers/[id]/route.ts +34 -0
  63. package/src/app/api/providers/configs/route.ts +7 -0
  64. package/src/app/api/providers/ollama/route.ts +30 -0
  65. package/src/app/api/providers/openclaw/health/route.ts +23 -0
  66. package/src/app/api/providers/route.ts +28 -0
  67. package/src/app/api/runs/[id]/route.ts +9 -0
  68. package/src/app/api/runs/route.ts +13 -0
  69. package/src/app/api/schedules/[id]/route.ts +28 -0
  70. package/src/app/api/schedules/[id]/run/route.ts +104 -0
  71. package/src/app/api/schedules/route.ts +78 -0
  72. package/src/app/api/secrets/[id]/route.ts +29 -0
  73. package/src/app/api/secrets/route.ts +42 -0
  74. package/src/app/api/sessions/[id]/browser/route.ts +13 -0
  75. package/src/app/api/sessions/[id]/chat/route.ts +96 -0
  76. package/src/app/api/sessions/[id]/clear/route.ts +19 -0
  77. package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
  78. package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
  79. package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
  80. package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
  81. package/src/app/api/sessions/[id]/messages/route.ts +9 -0
  82. package/src/app/api/sessions/[id]/retry/route.ts +28 -0
  83. package/src/app/api/sessions/[id]/route.ts +103 -0
  84. package/src/app/api/sessions/[id]/stop/route.ts +13 -0
  85. package/src/app/api/sessions/heartbeat/route.ts +26 -0
  86. package/src/app/api/sessions/route.ts +85 -0
  87. package/src/app/api/settings/route.ts +58 -0
  88. package/src/app/api/setup/check-provider/route.ts +326 -0
  89. package/src/app/api/setup/doctor/route.ts +250 -0
  90. package/src/app/api/skills/[id]/route.ts +40 -0
  91. package/src/app/api/skills/import/route.ts +69 -0
  92. package/src/app/api/skills/route.ts +28 -0
  93. package/src/app/api/tasks/[id]/route.ts +102 -0
  94. package/src/app/api/tasks/route.ts +115 -0
  95. package/src/app/api/tts/route.ts +40 -0
  96. package/src/app/api/upload/route.ts +18 -0
  97. package/src/app/api/uploads/[filename]/route.ts +59 -0
  98. package/src/app/api/usage/route.ts +35 -0
  99. package/src/app/api/version/route.ts +81 -0
  100. package/src/app/api/version/update/route.ts +95 -0
  101. package/src/app/api/webhooks/[id]/history/route.ts +13 -0
  102. package/src/app/api/webhooks/[id]/route.ts +204 -0
  103. package/src/app/api/webhooks/route.ts +37 -0
  104. package/src/app/favicon.ico +0 -0
  105. package/src/app/globals.css +370 -0
  106. package/src/app/layout.tsx +52 -0
  107. package/src/app/page.tsx +172 -0
  108. package/src/cli/index.js +1232 -0
  109. package/src/cli/index.test.js +281 -0
  110. package/src/cli/index.ts +1158 -0
  111. package/src/cli/spec.js +284 -0
  112. package/src/components/agents/agent-card.tsx +219 -0
  113. package/src/components/agents/agent-chat-list.tsx +165 -0
  114. package/src/components/agents/agent-list.tsx +110 -0
  115. package/src/components/agents/agent-sheet.tsx +1220 -0
  116. package/src/components/auth/access-key-gate.tsx +248 -0
  117. package/src/components/auth/setup-wizard.tsx +940 -0
  118. package/src/components/auth/user-picker.tsx +88 -0
  119. package/src/components/chat/chat-area.tsx +406 -0
  120. package/src/components/chat/chat-header.tsx +491 -0
  121. package/src/components/chat/chat-tool-toggles.tsx +161 -0
  122. package/src/components/chat/code-block.tsx +146 -0
  123. package/src/components/chat/dev-server-bar.tsx +39 -0
  124. package/src/components/chat/message-bubble.tsx +486 -0
  125. package/src/components/chat/message-list.tsx +299 -0
  126. package/src/components/chat/session-debug-panel.tsx +196 -0
  127. package/src/components/chat/streaming-bubble.tsx +85 -0
  128. package/src/components/chat/thinking-indicator.tsx +26 -0
  129. package/src/components/chat/tool-call-bubble.tsx +438 -0
  130. package/src/components/chat/tool-request-banner.tsx +103 -0
  131. package/src/components/connectors/connector-list.tsx +196 -0
  132. package/src/components/connectors/connector-sheet.tsx +804 -0
  133. package/src/components/input/chat-input.tsx +235 -0
  134. package/src/components/knowledge/knowledge-list.tsx +206 -0
  135. package/src/components/knowledge/knowledge-sheet.tsx +316 -0
  136. package/src/components/layout/app-layout.tsx +1016 -0
  137. package/src/components/layout/daemon-indicator.tsx +56 -0
  138. package/src/components/layout/mobile-header.tsx +31 -0
  139. package/src/components/layout/network-banner.tsx +17 -0
  140. package/src/components/layout/update-banner.tsx +130 -0
  141. package/src/components/logs/log-list.tsx +358 -0
  142. package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
  143. package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
  144. package/src/components/memory/memory-card.tsx +63 -0
  145. package/src/components/memory/memory-detail.tsx +339 -0
  146. package/src/components/memory/memory-list.tsx +198 -0
  147. package/src/components/memory/memory-sheet.tsx +70 -0
  148. package/src/components/plugins/plugin-list.tsx +60 -0
  149. package/src/components/plugins/plugin-sheet.tsx +311 -0
  150. package/src/components/providers/provider-list.tsx +96 -0
  151. package/src/components/providers/provider-sheet.tsx +542 -0
  152. package/src/components/runs/run-list.tsx +231 -0
  153. package/src/components/schedules/schedule-card.tsx +63 -0
  154. package/src/components/schedules/schedule-list.tsx +76 -0
  155. package/src/components/schedules/schedule-sheet.tsx +336 -0
  156. package/src/components/secrets/secret-sheet.tsx +180 -0
  157. package/src/components/secrets/secrets-list.tsx +91 -0
  158. package/src/components/sessions/new-session-sheet.tsx +478 -0
  159. package/src/components/sessions/session-card.tsx +144 -0
  160. package/src/components/sessions/session-list.tsx +202 -0
  161. package/src/components/shared/ai-gen-block.tsx +77 -0
  162. package/src/components/shared/avatar.tsx +48 -0
  163. package/src/components/shared/bottom-sheet.tsx +30 -0
  164. package/src/components/shared/confirm-dialog.tsx +47 -0
  165. package/src/components/shared/connector-platform-icon.tsx +113 -0
  166. package/src/components/shared/dir-browser.tsx +285 -0
  167. package/src/components/shared/dropdown.tsx +55 -0
  168. package/src/components/shared/icon-button.tsx +25 -0
  169. package/src/components/shared/settings/plugin-manager.tsx +207 -0
  170. package/src/components/shared/settings/section-capability-policy.tsx +93 -0
  171. package/src/components/shared/settings/section-embedding.tsx +99 -0
  172. package/src/components/shared/settings/section-heartbeat.tsx +168 -0
  173. package/src/components/shared/settings/section-memory.tsx +77 -0
  174. package/src/components/shared/settings/section-orchestrator.tsx +108 -0
  175. package/src/components/shared/settings/section-providers.tsx +181 -0
  176. package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
  177. package/src/components/shared/settings/section-secrets.tsx +132 -0
  178. package/src/components/shared/settings/section-user-preferences.tsx +24 -0
  179. package/src/components/shared/settings/section-voice.tsx +53 -0
  180. package/src/components/shared/settings/settings-sheet.tsx +88 -0
  181. package/src/components/shared/settings/types.ts +7 -0
  182. package/src/components/shared/settings/utils.ts +13 -0
  183. package/src/components/shared/settings-sheet.tsx +1 -0
  184. package/src/components/shared/skeleton.tsx +19 -0
  185. package/src/components/shared/usage-badge.tsx +28 -0
  186. package/src/components/skills/clawhub-browser.tsx +225 -0
  187. package/src/components/skills/skill-list.tsx +70 -0
  188. package/src/components/skills/skill-sheet.tsx +254 -0
  189. package/src/components/tasks/task-board.tsx +96 -0
  190. package/src/components/tasks/task-card.tsx +179 -0
  191. package/src/components/tasks/task-column.tsx +73 -0
  192. package/src/components/tasks/task-list.tsx +118 -0
  193. package/src/components/tasks/task-sheet.tsx +415 -0
  194. package/src/components/ui/avatar.tsx +109 -0
  195. package/src/components/ui/badge.tsx +48 -0
  196. package/src/components/ui/button.tsx +64 -0
  197. package/src/components/ui/card.tsx +92 -0
  198. package/src/components/ui/dialog.tsx +158 -0
  199. package/src/components/ui/dropdown-menu.tsx +257 -0
  200. package/src/components/ui/input.tsx +21 -0
  201. package/src/components/ui/scroll-area.tsx +58 -0
  202. package/src/components/ui/select.tsx +190 -0
  203. package/src/components/ui/separator.tsx +28 -0
  204. package/src/components/ui/sheet.tsx +143 -0
  205. package/src/components/ui/sonner.tsx +22 -0
  206. package/src/components/ui/textarea.tsx +18 -0
  207. package/src/components/ui/tooltip.tsx +56 -0
  208. package/src/components/usage/usage-list.tsx +105 -0
  209. package/src/components/webhooks/webhook-list.tsx +166 -0
  210. package/src/components/webhooks/webhook-sheet.tsx +402 -0
  211. package/src/hooks/use-auto-resize.ts +20 -0
  212. package/src/hooks/use-media-query.ts +21 -0
  213. package/src/hooks/use-speech-recognition.ts +83 -0
  214. package/src/instrumentation.ts +8 -0
  215. package/src/lib/agents.ts +13 -0
  216. package/src/lib/api-client.ts +100 -0
  217. package/src/lib/chat.ts +60 -0
  218. package/src/lib/memory.ts +42 -0
  219. package/src/lib/openclaw-endpoint.test.ts +48 -0
  220. package/src/lib/openclaw-endpoint.ts +67 -0
  221. package/src/lib/provider-config.ts +13 -0
  222. package/src/lib/providers/anthropic.ts +135 -0
  223. package/src/lib/providers/claude-cli.ts +202 -0
  224. package/src/lib/providers/codex-cli.ts +260 -0
  225. package/src/lib/providers/index.ts +351 -0
  226. package/src/lib/providers/ollama.ts +131 -0
  227. package/src/lib/providers/openai.ts +164 -0
  228. package/src/lib/providers/openclaw.ts +330 -0
  229. package/src/lib/providers/opencode-cli.ts +164 -0
  230. package/src/lib/runtime-loop.ts +15 -0
  231. package/src/lib/schedule-dedupe.test.ts +84 -0
  232. package/src/lib/schedule-dedupe.ts +174 -0
  233. package/src/lib/schedule-name.ts +62 -0
  234. package/src/lib/schedules.ts +16 -0
  235. package/src/lib/server/agent-registry.ts +70 -0
  236. package/src/lib/server/api-routes.test.ts +362 -0
  237. package/src/lib/server/autonomy-contract.ts +200 -0
  238. package/src/lib/server/build-llm.ts +155 -0
  239. package/src/lib/server/capability-router.test.ts +21 -0
  240. package/src/lib/server/capability-router.ts +172 -0
  241. package/src/lib/server/chat-execution.ts +894 -0
  242. package/src/lib/server/clawhub-client.test.ts +161 -0
  243. package/src/lib/server/clawhub-client.ts +26 -0
  244. package/src/lib/server/connectors/connector-routing.test.ts +243 -0
  245. package/src/lib/server/connectors/discord.ts +116 -0
  246. package/src/lib/server/connectors/googlechat.ts +66 -0
  247. package/src/lib/server/connectors/manager.ts +559 -0
  248. package/src/lib/server/connectors/matrix.ts +78 -0
  249. package/src/lib/server/connectors/media.ts +149 -0
  250. package/src/lib/server/connectors/openclaw.test.ts +375 -0
  251. package/src/lib/server/connectors/openclaw.ts +1132 -0
  252. package/src/lib/server/connectors/signal.ts +183 -0
  253. package/src/lib/server/connectors/slack.ts +258 -0
  254. package/src/lib/server/connectors/teams.ts +94 -0
  255. package/src/lib/server/connectors/telegram.ts +221 -0
  256. package/src/lib/server/connectors/types.ts +62 -0
  257. package/src/lib/server/connectors/whatsapp.ts +349 -0
  258. package/src/lib/server/context-manager.ts +232 -0
  259. package/src/lib/server/cost.ts +31 -0
  260. package/src/lib/server/daemon-state.ts +354 -0
  261. package/src/lib/server/data-dir.ts +3 -0
  262. package/src/lib/server/embeddings.ts +111 -0
  263. package/src/lib/server/execution-log.ts +257 -0
  264. package/src/lib/server/gateway/protocol.test.ts +54 -0
  265. package/src/lib/server/gateway/protocol.ts +114 -0
  266. package/src/lib/server/heartbeat-service.ts +366 -0
  267. package/src/lib/server/knowledge-db.test.ts +441 -0
  268. package/src/lib/server/logger.ts +47 -0
  269. package/src/lib/server/main-agent-loop.ts +1017 -0
  270. package/src/lib/server/mcp-client.test.ts +342 -0
  271. package/src/lib/server/mcp-client.ts +130 -0
  272. package/src/lib/server/memory-db.ts +1078 -0
  273. package/src/lib/server/memory-graph.test.ts +153 -0
  274. package/src/lib/server/memory-graph.ts +138 -0
  275. package/src/lib/server/openclaw-health.ts +245 -0
  276. package/src/lib/server/orchestrator-lg.ts +431 -0
  277. package/src/lib/server/orchestrator.ts +364 -0
  278. package/src/lib/server/playwright-proxy.mjs +70 -0
  279. package/src/lib/server/plugins.ts +229 -0
  280. package/src/lib/server/process-manager.ts +327 -0
  281. package/src/lib/server/provider-health.ts +113 -0
  282. package/src/lib/server/queue.ts +859 -0
  283. package/src/lib/server/runtime-settings.ts +119 -0
  284. package/src/lib/server/scheduler.ts +196 -0
  285. package/src/lib/server/session-mailbox.ts +129 -0
  286. package/src/lib/server/session-run-manager.ts +512 -0
  287. package/src/lib/server/session-tools/connector.ts +124 -0
  288. package/src/lib/server/session-tools/context-mgmt.ts +103 -0
  289. package/src/lib/server/session-tools/context.ts +114 -0
  290. package/src/lib/server/session-tools/crud.ts +673 -0
  291. package/src/lib/server/session-tools/delegate.ts +708 -0
  292. package/src/lib/server/session-tools/file.ts +264 -0
  293. package/src/lib/server/session-tools/index.ts +164 -0
  294. package/src/lib/server/session-tools/memory.ts +230 -0
  295. package/src/lib/server/session-tools/session-info.ts +422 -0
  296. package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
  297. package/src/lib/server/session-tools/shell.ts +171 -0
  298. package/src/lib/server/session-tools/web.ts +408 -0
  299. package/src/lib/server/session-tools.ts +9 -0
  300. package/src/lib/server/skills-normalize.ts +130 -0
  301. package/src/lib/server/storage-mcp.test.ts +161 -0
  302. package/src/lib/server/storage.ts +670 -0
  303. package/src/lib/server/stream-agent-chat.ts +571 -0
  304. package/src/lib/server/task-reports.ts +122 -0
  305. package/src/lib/server/task-result.ts +161 -0
  306. package/src/lib/server/task-validation.test.ts +27 -0
  307. package/src/lib/server/task-validation.ts +90 -0
  308. package/src/lib/server/tool-capability-policy.test.ts +58 -0
  309. package/src/lib/server/tool-capability-policy.ts +262 -0
  310. package/src/lib/sessions.ts +68 -0
  311. package/src/lib/tasks.ts +20 -0
  312. package/src/lib/tts.ts +42 -0
  313. package/src/lib/upload.ts +10 -0
  314. package/src/lib/utils.ts +6 -0
  315. package/src/proxy.ts +43 -0
  316. package/src/stores/use-app-store.ts +468 -0
  317. package/src/stores/use-chat-store.ts +323 -0
  318. package/src/types/index.ts +621 -0
  319. package/tsconfig.json +34 -0
@@ -0,0 +1,339 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useCallback } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { getMemory, updateMemory, deleteMemory } from '@/lib/memory'
6
+ import type { MemoryEntry } from '@/types'
7
+
8
+ const CATEGORIES = ['note', 'fact', 'preference', 'finding', 'learning', 'general']
9
+
10
+ export function MemoryDetail() {
11
+ const selectedId = useAppStore((s) => s.selectedMemoryId)
12
+ const setSelectedId = useAppStore((s) => s.setSelectedMemoryId)
13
+ const triggerRefresh = useAppStore((s) => s.triggerMemoryRefresh)
14
+ const agents = useAppStore((s) => s.agents)
15
+ const sessions = useAppStore((s) => s.sessions)
16
+ const setCurrentSession = useAppStore((s) => s.setCurrentSession)
17
+ const setActiveView = useAppStore((s) => s.setActiveView)
18
+
19
+ const [entry, setEntry] = useState<MemoryEntry | null>(null)
20
+ const [title, setTitle] = useState('')
21
+ const [content, setContent] = useState('')
22
+ const [category, setCategory] = useState('note')
23
+ const [dirty, setDirty] = useState(false)
24
+ const [saving, setSaving] = useState(false)
25
+ const [confirmDelete, setConfirmDelete] = useState(false)
26
+
27
+ // Load memory entry when selection changes
28
+ useEffect(() => {
29
+ if (!selectedId) {
30
+ setEntry(null)
31
+ return
32
+ }
33
+
34
+ let cancelled = false
35
+ getMemory(selectedId, { depth: 0 })
36
+ .then((found) => {
37
+ if (cancelled || !found) return
38
+
39
+ const resolved = Array.isArray(found)
40
+ ? found.find((item) => item.id === selectedId) || found[0] || null
41
+ : found
42
+
43
+ if (!resolved) return
44
+
45
+ setEntry(resolved)
46
+ setTitle(resolved.title)
47
+ setContent(resolved.content)
48
+ setCategory(resolved.category || 'note')
49
+ setDirty(false)
50
+ })
51
+ .catch((err) => console.error('Memory operation failed:', err))
52
+
53
+ return () => {
54
+ cancelled = true
55
+ }
56
+ }, [selectedId])
57
+
58
+ const handleSave = useCallback(async () => {
59
+ if (!entry || !dirty) return
60
+ setSaving(true)
61
+ try {
62
+ const updated = await updateMemory(entry.id, { title, content, category })
63
+ setEntry(updated)
64
+ setDirty(false)
65
+ triggerRefresh()
66
+ } catch { /* ignore */ }
67
+ setSaving(false)
68
+ }, [entry, title, content, category, dirty])
69
+
70
+ const handleDelete = useCallback(async () => {
71
+ if (!entry) return
72
+ await deleteMemory(entry.id)
73
+ setSelectedId(null)
74
+ triggerRefresh()
75
+ }, [entry])
76
+
77
+ const handleNavigateToSession = useCallback(() => {
78
+ if (!entry?.sessionId) return
79
+ setActiveView('sessions')
80
+ setCurrentSession(entry.sessionId)
81
+ }, [entry])
82
+
83
+ if (!entry) {
84
+ return (
85
+ <div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
86
+ <div className="w-14 h-14 rounded-[16px] bg-white/[0.03] flex items-center justify-center mb-2">
87
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/60">
88
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
89
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
90
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
91
+ </svg>
92
+ </div>
93
+ <p className="font-display text-[17px] font-600 text-text-2">Memory</p>
94
+ <p className="text-[13px] text-text-3/70 max-w-[300px]">
95
+ Select a memory from the sidebar to view and edit it
96
+ </p>
97
+ </div>
98
+ )
99
+ }
100
+
101
+ const agentName = entry.agentId ? (agents[entry.agentId]?.name || entry.agentId) : null
102
+ const sessionName = entry.sessionId ? (sessions[entry.sessionId]?.name || entry.sessionId) : null
103
+ const imagePath = entry.image?.path || entry.imagePath || null
104
+ const imageUrl = imagePath
105
+ ? imagePath.startsWith('data/memory-images/')
106
+ ? `/api/memory-images/${imagePath.split('/').pop()}`
107
+ : imagePath
108
+ : null
109
+
110
+ const inputClass = "w-full px-4 py-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] text-text outline-none transition-all duration-200 placeholder:text-text-3/70 focus:border-accent-bright/20 focus:bg-white/[0.03]"
111
+
112
+ return (
113
+ <div className="flex-1 flex flex-col h-full min-h-0">
114
+ {/* Header */}
115
+ <div className="shrink-0 px-6 py-4 border-b border-white/[0.04] flex items-center gap-3">
116
+ <div className="flex-1 min-w-0">
117
+ <div className="flex items-center gap-2.5">
118
+ <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]">
119
+ {category}
120
+ </span>
121
+ <h2 className="font-display text-[16px] font-700 truncate tracking-[-0.02em]">{title || 'Untitled'}</h2>
122
+ </div>
123
+ <div className="flex items-center gap-3 mt-1">
124
+ {agentName && (
125
+ <span className="text-[11px] text-text-3/50 flex items-center gap-1">
126
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
127
+ {agentName}
128
+ </span>
129
+ )}
130
+ {sessionName && (
131
+ <button
132
+ onClick={handleNavigateToSession}
133
+ className="text-[11px] text-accent-bright/50 hover:text-accent-bright flex items-center gap-1 bg-transparent border-none cursor-pointer p-0 transition-colors"
134
+ >
135
+ <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /></svg>
136
+ {sessionName}
137
+ </button>
138
+ )}
139
+ <span className="text-[10px] text-text-3/50 font-mono tabular-nums">
140
+ {new Date(entry.createdAt).toLocaleString()}
141
+ </span>
142
+ </div>
143
+ </div>
144
+
145
+ <div className="flex items-center gap-2 shrink-0">
146
+ {dirty && (
147
+ <button
148
+ onClick={handleSave}
149
+ disabled={saving}
150
+ className="px-4 py-2 rounded-[10px] bg-[#6366F1] text-white text-[12px] font-600
151
+ cursor-pointer border-none transition-all hover:brightness-110 active:scale-[0.97]
152
+ disabled:opacity-50 shadow-[0_2px_10px_rgba(99,102,241,0.2)]"
153
+ style={{ fontFamily: 'inherit' }}
154
+ >
155
+ {saving ? 'Saving...' : 'Save'}
156
+ </button>
157
+ )}
158
+ <button
159
+ onClick={() => setConfirmDelete(true)}
160
+ className="p-2 rounded-[8px] text-text-3/70 hover:text-red-400 hover:bg-red-400/[0.06]
161
+ cursor-pointer transition-all bg-transparent border-none"
162
+ title="Delete memory"
163
+ >
164
+ <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
165
+ <polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
166
+ <path d="M10 11v6" /><path d="M14 11v6" /><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
167
+ </svg>
168
+ </button>
169
+ </div>
170
+ </div>
171
+
172
+ {/* Edit form */}
173
+ <div className="flex-1 overflow-y-auto px-6 py-5">
174
+ <div className="max-w-[640px] space-y-5">
175
+ {/* Title */}
176
+ <div>
177
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Title</label>
178
+ <input
179
+ type="text"
180
+ value={title}
181
+ onChange={(e) => { setTitle(e.target.value); setDirty(true) }}
182
+ className={`${inputClass} text-[15px] font-600`}
183
+ style={{ fontFamily: 'inherit' }}
184
+ placeholder="Memory title"
185
+ />
186
+ </div>
187
+
188
+ {/* Category */}
189
+ <div>
190
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Category</label>
191
+ <div className="flex gap-1.5 flex-wrap">
192
+ {CATEGORIES.map((c) => (
193
+ <button
194
+ key={c}
195
+ onClick={() => { setCategory(c); setDirty(true) }}
196
+ className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all border-none
197
+ ${category === c
198
+ ? 'bg-accent-soft text-accent-bright'
199
+ : 'bg-white/[0.03] text-text-3 hover:text-text-2 hover:bg-white/[0.05]'}`}
200
+ style={{ fontFamily: 'inherit' }}
201
+ >
202
+ {c}
203
+ </button>
204
+ ))}
205
+ </div>
206
+ </div>
207
+
208
+ {/* Content */}
209
+ <div>
210
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Content</label>
211
+ <textarea
212
+ value={content}
213
+ onChange={(e) => { setContent(e.target.value); setDirty(true) }}
214
+ placeholder="Memory content..."
215
+ rows={12}
216
+ className={`${inputClass} text-[14px] resize-y min-h-[200px] leading-relaxed`}
217
+ style={{ fontFamily: 'inherit' }}
218
+ />
219
+ </div>
220
+
221
+ {imageUrl && (
222
+ <div>
223
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Image</label>
224
+ <a href={imageUrl} target="_blank" rel="noreferrer" className="inline-block rounded-[12px] overflow-hidden border border-white/[0.08]">
225
+ <img src={imageUrl} alt={entry.title} className="max-w-[320px] max-h-[220px] object-cover block" />
226
+ </a>
227
+ </div>
228
+ )}
229
+
230
+ {entry.references?.length ? (
231
+ <div>
232
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">References</label>
233
+ <div className="space-y-2">
234
+ {entry.references.map((ref, idx) => (
235
+ <div key={`${ref.type}-${ref.path || ref.title || idx}`} className="text-[12px] rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2">
236
+ <div className="text-text-2/70">
237
+ <span className="uppercase text-[10px] tracking-[0.06em] mr-1">{ref.type}</span>
238
+ {ref.path || ref.title || '(no path)'}
239
+ </div>
240
+ {(ref.projectName || ref.projectRoot || ref.note || typeof ref.exists === 'boolean') && (
241
+ <div className="text-text-3/55 mt-1">
242
+ {ref.projectName ? `project: ${ref.projectName} ` : ''}
243
+ {ref.projectRoot ? `root: ${ref.projectRoot} ` : ''}
244
+ {typeof ref.exists === 'boolean' ? (ref.exists ? 'exists' : 'missing') : ''}
245
+ {ref.note ? ` — ${ref.note}` : ''}
246
+ </div>
247
+ )}
248
+ </div>
249
+ ))}
250
+ </div>
251
+ </div>
252
+ ) : null}
253
+
254
+ {entry.linkedMemoryIds?.length ? (
255
+ <div>
256
+ <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Linked Memories</label>
257
+ <div className="flex flex-wrap gap-1.5">
258
+ {entry.linkedMemoryIds.map((id) => (
259
+ <button
260
+ key={id}
261
+ onClick={() => setSelectedId(id)}
262
+ className="px-2.5 py-1 rounded-[8px] text-[11px] font-mono bg-white/[0.04] border border-white/[0.08] text-accent-bright/70 hover:text-accent-bright cursor-pointer transition-colors"
263
+ >
264
+ {id}
265
+ </button>
266
+ ))}
267
+ </div>
268
+ </div>
269
+ ) : null}
270
+
271
+ {/* Metadata */}
272
+ <div className="pt-4 border-t border-white/[0.04]">
273
+ <div className="grid grid-cols-2 gap-4 text-[11px]">
274
+ <div>
275
+ <span className="text-text-3/70 block mb-1">ID</span>
276
+ <span className="text-text-3/60 font-mono">{entry.id}</span>
277
+ </div>
278
+ <div>
279
+ <span className="text-text-3/70 block mb-1">Created</span>
280
+ <span className="text-text-3/60 font-mono">{new Date(entry.createdAt).toLocaleString()}</span>
281
+ </div>
282
+ <div>
283
+ <span className="text-text-3/70 block mb-1">Updated</span>
284
+ <span className="text-text-3/60 font-mono">{new Date(entry.updatedAt).toLocaleString()}</span>
285
+ </div>
286
+ {entry.agentId && (
287
+ <div>
288
+ <span className="text-text-3/70 block mb-1">Agent</span>
289
+ <span className="text-text-3/60 font-mono">{agentName}</span>
290
+ </div>
291
+ )}
292
+ {entry.sessionId && (
293
+ <div>
294
+ <span className="text-text-3/70 block mb-1">Session</span>
295
+ <button
296
+ onClick={handleNavigateToSession}
297
+ className="text-accent-bright/60 hover:text-accent-bright font-mono bg-transparent border-none cursor-pointer p-0 text-[11px] transition-colors"
298
+ >
299
+ {sessionName}
300
+ </button>
301
+ </div>
302
+ )}
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </div>
307
+
308
+ {/* Delete confirmation */}
309
+ {confirmDelete && (
310
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
311
+ <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setConfirmDelete(false)} />
312
+ <div className="relative bg-raised rounded-[16px] p-6 max-w-[360px] w-full shadow-xl border border-white/[0.06]"
313
+ style={{ animation: 'fade-in 0.15s cubic-bezier(0.16, 1, 0.3, 1)' }}>
314
+ <h3 className="font-display text-[16px] font-700 mb-2">Delete Memory</h3>
315
+ <p className="text-[13px] text-text-3 mb-5">
316
+ Delete &ldquo;{entry.title}&rdquo;? This cannot be undone.
317
+ </p>
318
+ <div className="flex gap-3">
319
+ <button
320
+ onClick={() => setConfirmDelete(false)}
321
+ 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"
322
+ style={{ fontFamily: 'inherit' }}
323
+ >
324
+ Cancel
325
+ </button>
326
+ <button
327
+ onClick={handleDelete}
328
+ 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"
329
+ style={{ fontFamily: 'inherit' }}
330
+ >
331
+ Delete
332
+ </button>
333
+ </div>
334
+ </div>
335
+ </div>
336
+ )}
337
+ </div>
338
+ )
339
+ }
@@ -0,0 +1,198 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
4
+ import { searchMemory } from '@/lib/memory'
5
+ import { useAppStore } from '@/stores/use-app-store'
6
+ import { MemoryCard } from './memory-card'
7
+ import type { MemoryEntry } from '@/types'
8
+
9
+ interface Props {
10
+ inSidebar?: boolean
11
+ onSelect?: () => void
12
+ }
13
+
14
+ export function MemoryList({ inSidebar: _inSidebar, onSelect }: Props) {
15
+ void _inSidebar
16
+ const selectedMemoryId = useAppStore((s) => s.selectedMemoryId)
17
+ const setSelectedMemoryId = useAppStore((s) => s.setSelectedMemoryId)
18
+ const refreshKey = useAppStore((s) => s.memoryRefreshKey)
19
+ const agents = useAppStore((s) => s.agents)
20
+ const memoryAgentFilter = useAppStore((s) => s.memoryAgentFilter)
21
+ const setMemoryAgentFilter = useAppStore((s) => s.setMemoryAgentFilter)
22
+ const [search, setSearch] = useState('')
23
+ const [entries, setEntries] = useState<MemoryEntry[]>([])
24
+ const [loaded, setLoaded] = useState(false)
25
+ const [error, setError] = useState<string | null>(null)
26
+ const [categoryFilter, setCategoryFilter] = useState<string>('')
27
+ const searchRef = useRef(search)
28
+
29
+ const load = useCallback(async (query: string) => {
30
+ try {
31
+ const results = await searchMemory({ q: query || undefined })
32
+ setEntries(Array.isArray(results) ? results : [])
33
+ setError(null)
34
+ } catch {
35
+ setError('Unable to load memories right now.')
36
+ }
37
+ setLoaded(true)
38
+ }, [])
39
+
40
+ useEffect(() => {
41
+ searchRef.current = search
42
+ }, [search])
43
+
44
+ useEffect(() => {
45
+ const timer = setTimeout(() => { void load(searchRef.current) }, 0)
46
+ return () => clearTimeout(timer)
47
+ }, [refreshKey, load])
48
+
49
+ useEffect(() => {
50
+ const timer = setTimeout(() => { void load(search) }, 300)
51
+ return () => clearTimeout(timer)
52
+ }, [search, load])
53
+
54
+ // Derive unique agents and categories
55
+ const uniqueAgents = useMemo(() => {
56
+ const map = new Map<string, number>()
57
+ for (const e of entries) {
58
+ const key = e.agentId || '_global'
59
+ map.set(key, (map.get(key) || 0) + 1)
60
+ }
61
+ return map
62
+ }, [entries])
63
+
64
+ const uniqueCategories = useMemo(() => {
65
+ const cats = new Set<string>()
66
+ for (const e of entries) cats.add(e.category || 'note')
67
+ return Array.from(cats).sort()
68
+ }, [entries])
69
+
70
+ const filtered = useMemo(() => {
71
+ return entries.filter((e) => {
72
+ if (memoryAgentFilter && (e.agentId || null) !== memoryAgentFilter) return false
73
+ if (categoryFilter && (e.category || 'note') !== categoryFilter) return false
74
+ return true
75
+ })
76
+ }, [entries, memoryAgentFilter, categoryFilter])
77
+
78
+ const hasMultipleAgents = uniqueAgents.size > 1 || (uniqueAgents.size === 1 && !uniqueAgents.has('_global'))
79
+
80
+ return (
81
+ <div className="flex-1 flex flex-col overflow-y-auto">
82
+ {/* Search */}
83
+ <div className="px-3 py-2 shrink-0">
84
+ <input
85
+ type="text"
86
+ value={search}
87
+ onChange={(e) => setSearch(e.target.value)}
88
+ placeholder="Search memories..."
89
+ className="w-full px-3 py-2 rounded-[10px] border border-white/[0.04] bg-surface text-text
90
+ text-[12px] outline-none transition-all duration-200 placeholder:text-text-3/70 focus-glow"
91
+ style={{ fontFamily: 'inherit' }}
92
+ />
93
+ </div>
94
+
95
+ {/* Agent filter tabs */}
96
+ {entries.length > 0 && hasMultipleAgents && (
97
+ <div className="px-3 pb-1.5 shrink-0">
98
+ <div className="flex gap-1 flex-wrap">
99
+ <button
100
+ onClick={() => setMemoryAgentFilter(null)}
101
+ className={`px-2.5 py-1 rounded-[7px] text-[10px] font-600 cursor-pointer transition-all
102
+ ${!memoryAgentFilter ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
103
+ style={{ fontFamily: 'inherit' }}
104
+ >
105
+ All ({entries.length})
106
+ </button>
107
+ {Array.from(uniqueAgents.entries()).map(([agentId, count]) => {
108
+ const id = agentId === '_global' ? null : agentId
109
+ const name = id ? (agents[id]?.name || id.slice(0, 8)) : 'Global'
110
+ return (
111
+ <button
112
+ key={agentId}
113
+ onClick={() => setMemoryAgentFilter(id)}
114
+ className={`px-2.5 py-1 rounded-[7px] text-[10px] font-600 cursor-pointer transition-all truncate max-w-[120px]
115
+ ${memoryAgentFilter === id ? 'bg-accent-soft text-accent-bright' : 'bg-transparent text-text-3 hover:text-text-2'}`}
116
+ style={{ fontFamily: 'inherit' }}
117
+ >
118
+ {name} ({count})
119
+ </button>
120
+ )
121
+ })}
122
+ </div>
123
+ </div>
124
+ )}
125
+
126
+ {/* Category filter */}
127
+ {entries.length > 0 && uniqueCategories.length > 1 && (
128
+ <div className="px-3 pb-1.5 shrink-0">
129
+ <div className="flex gap-1 flex-wrap">
130
+ <button
131
+ onClick={() => setCategoryFilter('')}
132
+ className={`px-2 py-0.5 rounded-[6px] text-[9px] font-600 cursor-pointer transition-all uppercase tracking-wider
133
+ ${!categoryFilter ? 'bg-white/[0.06] text-text-2' : 'bg-transparent text-text-3/70 hover:text-text-3'}`}
134
+ style={{ fontFamily: 'inherit' }}
135
+ >
136
+ all
137
+ </button>
138
+ {uniqueCategories.map((c) => (
139
+ <button
140
+ key={c}
141
+ onClick={() => setCategoryFilter(categoryFilter === c ? '' : c)}
142
+ className={`px-2 py-0.5 rounded-[6px] text-[9px] font-600 cursor-pointer transition-all uppercase tracking-wider
143
+ ${categoryFilter === c ? 'bg-white/[0.06] text-text-2' : 'bg-transparent text-text-3/70 hover:text-text-3'}`}
144
+ style={{ fontFamily: 'inherit' }}
145
+ >
146
+ {c}
147
+ </button>
148
+ ))}
149
+ </div>
150
+ </div>
151
+ )}
152
+
153
+ {/* Memory cards */}
154
+ {filtered.length > 0 ? (
155
+ <div className="flex flex-col gap-0.5 px-2 pb-4">
156
+ {filtered.map((e) => (
157
+ <MemoryCard
158
+ key={e.id}
159
+ entry={e}
160
+ active={e.id === selectedMemoryId}
161
+ agentName={e.agentId ? (agents[e.agentId]?.name || null) : null}
162
+ onClick={() => {
163
+ setSelectedMemoryId(e.id)
164
+ onSelect?.()
165
+ }}
166
+ />
167
+ ))}
168
+ </div>
169
+ ) : error ? (
170
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-3 p-8 text-center">
171
+ <p className="font-display text-[14px] font-600 text-text-2">Couldn&apos;t load memories</p>
172
+ <p className="text-[12px] text-text-3/60">{error}</p>
173
+ <button
174
+ onClick={() => { void load(search) }}
175
+ className="px-3 py-1.5 rounded-[8px] bg-accent-soft text-accent-bright text-[12px] font-600 cursor-pointer border-none"
176
+ style={{ fontFamily: 'inherit' }}
177
+ >
178
+ Retry
179
+ </button>
180
+ </div>
181
+ ) : loaded ? (
182
+ <div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
183
+ <div className="w-12 h-12 rounded-[14px] bg-accent-soft flex items-center justify-center mb-1">
184
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright">
185
+ <ellipse cx="12" cy="5" rx="9" ry="3" />
186
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
187
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
188
+ </svg>
189
+ </div>
190
+ <p className="font-display text-[15px] font-600 text-text-2">
191
+ {memoryAgentFilter ? 'No memories for this agent' : 'No memories yet'}
192
+ </p>
193
+ <p className="text-[13px] text-text-3/50">AI agents store knowledge here</p>
194
+ </div>
195
+ ) : null}
196
+ </div>
197
+ )
198
+ }
@@ -0,0 +1,70 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+ import { createMemory } from '@/lib/memory'
6
+ import { BottomSheet } from '@/components/shared/bottom-sheet'
7
+
8
+ export function MemorySheet() {
9
+ const open = useAppStore((s) => s.memorySheetOpen)
10
+ const setOpen = useAppStore((s) => s.setMemorySheetOpen)
11
+ const triggerRefresh = useAppStore((s) => s.triggerMemoryRefresh)
12
+
13
+ const [title, setTitle] = useState('')
14
+ const [content, setContent] = useState('')
15
+
16
+ const onClose = () => {
17
+ setOpen(false)
18
+ setTitle('')
19
+ setContent('')
20
+ }
21
+
22
+ const handleSave = async () => {
23
+ await createMemory({
24
+ title: title.trim() || 'Untitled',
25
+ category: 'general',
26
+ content,
27
+ agentId: null,
28
+ sessionId: null,
29
+ })
30
+ triggerRefresh()
31
+ onClose()
32
+ }
33
+
34
+ const inputClass = "w-full px-4 py-3.5 rounded-[14px] border border-white/[0.08] bg-surface text-text text-[15px] outline-none transition-all duration-200 placeholder:text-text-3/50 focus-glow"
35
+
36
+ return (
37
+ <BottomSheet open={open} onClose={onClose}>
38
+ <div className="mb-10">
39
+ <h2 className="font-display text-[28px] font-700 tracking-[-0.03em] mb-2">New Memory</h2>
40
+ <p className="text-[14px] text-text-3">Store a piece of knowledge</p>
41
+ </div>
42
+
43
+ <div className="mb-8">
44
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Title</label>
45
+ <input type="text" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Memory title" className={inputClass} style={{ fontFamily: 'inherit' }} />
46
+ </div>
47
+
48
+ <div className="mb-8">
49
+ <label className="block font-display text-[12px] font-600 text-text-2 uppercase tracking-[0.08em] mb-3">Content</label>
50
+ <textarea
51
+ value={content}
52
+ onChange={(e) => setContent(e.target.value)}
53
+ placeholder="Memory content..."
54
+ rows={6}
55
+ className={`${inputClass} resize-y min-h-[150px]`}
56
+ style={{ fontFamily: 'inherit' }}
57
+ />
58
+ </div>
59
+
60
+ <div className="flex gap-3 pt-2 border-t border-white/[0.04]">
61
+ <button onClick={onClose} className="flex-1 py-3.5 rounded-[14px] border border-white/[0.08] bg-transparent text-text-2 text-[15px] font-600 cursor-pointer hover:bg-surface-2 transition-all" style={{ fontFamily: 'inherit' }}>
62
+ Cancel
63
+ </button>
64
+ <button onClick={handleSave} disabled={!title.trim()} className="flex-1 py-3.5 rounded-[14px] border-none bg-[#6366F1] text-white text-[15px] font-600 cursor-pointer active:scale-[0.97] disabled:opacity-30 transition-all shadow-[0_4px_20px_rgba(99,102,241,0.25)] hover:brightness-110" style={{ fontFamily: 'inherit' }}>
65
+ Save
66
+ </button>
67
+ </div>
68
+ </BottomSheet>
69
+ )
70
+ }
@@ -0,0 +1,60 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useAppStore } from '@/stores/use-app-store'
5
+
6
+ export function PluginList({ inSidebar }: { inSidebar?: boolean }) {
7
+ const plugins = useAppStore((s) => s.plugins)
8
+ const loadPlugins = useAppStore((s) => s.loadPlugins)
9
+ const setPluginSheetOpen = useAppStore((s) => s.setPluginSheetOpen)
10
+ const setEditingPluginFilename = useAppStore((s) => s.setEditingPluginFilename)
11
+
12
+ useEffect(() => {
13
+ loadPlugins()
14
+ }, [])
15
+
16
+ const pluginList = Object.values(plugins)
17
+
18
+ const handleEdit = (filename: string) => {
19
+ setEditingPluginFilename(filename)
20
+ setPluginSheetOpen(true)
21
+ }
22
+
23
+ return (
24
+ <div className={`flex-1 overflow-y-auto ${inSidebar ? 'px-3 pb-4' : 'px-4'}`}>
25
+ {pluginList.length === 0 ? (
26
+ <div className="text-center py-12">
27
+ <p className="text-[13px] text-text-3/60">No plugins installed</p>
28
+ <button
29
+ onClick={() => { setEditingPluginFilename(null); setPluginSheetOpen(true) }}
30
+ className="mt-3 px-4 py-2 rounded-[10px] bg-transparent text-accent-bright text-[13px] font-600 cursor-pointer border border-accent-bright/20 hover:bg-accent-soft transition-all"
31
+ style={{ fontFamily: 'inherit' }}
32
+ >
33
+ + Add Plugin
34
+ </button>
35
+ </div>
36
+ ) : (
37
+ <div className="space-y-2">
38
+ {pluginList.map((plugin) => (
39
+ <button
40
+ key={plugin.filename}
41
+ onClick={() => handleEdit(plugin.filename)}
42
+ className="w-full text-left p-4 rounded-[14px] border border-white/[0.06] bg-surface hover:bg-surface-2 transition-all cursor-pointer"
43
+ >
44
+ <div className="flex items-center justify-between mb-1">
45
+ <span className="font-display text-[14px] font-600 text-text truncate">{plugin.name}</span>
46
+ <span className={`text-[10px] font-600 px-1.5 py-0.5 rounded-full shrink-0 ml-2 ${plugin.enabled ? 'text-emerald-400 bg-emerald-400/10' : 'text-text-3/50 bg-white/[0.04]'}`}>
47
+ {plugin.enabled ? 'Enabled' : 'Disabled'}
48
+ </span>
49
+ </div>
50
+ <div className="text-[11px] font-mono text-text-3/50 mb-1">{plugin.filename}</div>
51
+ {plugin.description && (
52
+ <p className="text-[12px] text-text-3/60 line-clamp-2">{plugin.description}</p>
53
+ )}
54
+ </button>
55
+ ))}
56
+ </div>
57
+ )}
58
+ </div>
59
+ )
60
+ }