@axplusb/kepler 0.0.1 → 1.0.1

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 (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +101 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. package/index.js +0 -1
@@ -0,0 +1,365 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import useSWR, { mutate } from 'swr'
5
+ import { TopBar } from '@/components/layout/top-bar'
6
+ import type { MemoryEntry, MemoryType } from '@/lib/claude-reader'
7
+ import { projectDisplayName, projectShortPath, formatRelativeDate } from '@/lib/decode'
8
+
9
+ const fetcher = (url: string) =>
10
+ fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
11
+
12
+ // ── Type config ───────────────────────────────────────────────────────────────
13
+
14
+ const TYPE_META: Record<MemoryType, { label: string; color: string; bg: string; border: string; dot: string }> = {
15
+ user: { label: 'user', color: 'text-blue-700 dark:text-blue-400', bg: 'bg-blue-700/10 dark:bg-blue-400/10', border: 'border-blue-700/30 dark:border-blue-400/30', dot: 'var(--viz-sky)' },
16
+ feedback: { label: 'feedback', color: 'text-[#f87171]', bg: 'bg-[#f87171]/10', border: 'border-[#f87171]/30', dot: '#f87171' },
17
+ project: { label: 'project', color: 'text-[#a78bfa]', bg: 'bg-[#a78bfa]/10', border: 'border-[#a78bfa]/30', dot: '#a78bfa' },
18
+ reference: { label: 'reference', color: 'text-[#34d399]', bg: 'bg-[#34d399]/10', border: 'border-[#34d399]/30', dot: '#34d399' },
19
+ index: { label: 'index', color: 'text-[#fbbf24]', bg: 'bg-[#fbbf24]/10', border: 'border-[#fbbf24]/30', dot: '#fbbf24' },
20
+ unknown: { label: '?', color: 'text-muted-foreground', bg: 'bg-muted', border: 'border-border', dot: '#94a3b8' },
21
+ }
22
+
23
+ const FILTER_TYPES = ['all', 'user', 'feedback', 'project', 'reference', 'index'] as const
24
+ type FilterType = typeof FILTER_TYPES[number]
25
+
26
+ function TypeBadge({ type }: { type: MemoryType }) {
27
+ const m = TYPE_META[type] ?? TYPE_META.unknown
28
+ return (
29
+ <span className={`text-[10px] font-mono font-bold uppercase tracking-wider px-1.5 py-0.5 rounded border ${m.color} ${m.bg} ${m.border}`}>
30
+ {m.label}
31
+ </span>
32
+ )
33
+ }
34
+
35
+ function StaleBadge({ mtime }: { mtime: string }) {
36
+ // eslint-disable-next-line react-hooks/purity
37
+ const daysOld = Math.floor((Date.now() - new Date(mtime).getTime()) / 86_400_000)
38
+ if (daysOld < 30) return null
39
+ return (
40
+ <span className="text-[10px] font-mono px-1.5 py-0.5 rounded border border-[#f87171]/30 bg-[#f87171]/10 text-[#f87171]">
41
+ stale
42
+ </span>
43
+ )
44
+ }
45
+
46
+ // ── Memory card ───────────────────────────────────────────────────────────────
47
+
48
+ function MemoryCard({ entry, onClick, expanded }: { entry: MemoryEntry; onClick: () => void; expanded: boolean }) {
49
+ const projectName = projectDisplayName(entry.projectPath)
50
+ const shortPath = projectShortPath(entry.projectPath)
51
+ const m = TYPE_META[entry.type] ?? TYPE_META.unknown
52
+ const preview = entry.body.slice(0, 200).replace(/\n+/g, ' ').trim()
53
+
54
+ const [editing, setEditing] = useState(false)
55
+ const [draft, setDraft] = useState(entry.body)
56
+ const [saving, setSaving] = useState(false)
57
+ const [saveError, setSaveError] = useState<string | null>(null)
58
+
59
+ async function handleSave(e: React.MouseEvent) {
60
+ e.stopPropagation()
61
+ setSaving(true)
62
+ setSaveError(null)
63
+ try {
64
+ const res = await fetch('/api/memory', {
65
+ method: 'PATCH',
66
+ headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({
68
+ projectSlug: entry.projectSlug,
69
+ file: entry.file,
70
+ content: draft,
71
+ }),
72
+ })
73
+ if (!res.ok) {
74
+ const { error } = await res.json()
75
+ setSaveError(error ?? 'Save failed')
76
+ } else {
77
+ setEditing(false)
78
+ mutate('/api/memory')
79
+ }
80
+ } catch (err) {
81
+ setSaveError(String(err))
82
+ } finally {
83
+ setSaving(false)
84
+ }
85
+ }
86
+
87
+ function handleEdit(e: React.MouseEvent) {
88
+ e.stopPropagation()
89
+ setDraft(entry.body)
90
+ setSaveError(null)
91
+ setEditing(true)
92
+ }
93
+
94
+ function handleCancel(e: React.MouseEvent) {
95
+ e.stopPropagation()
96
+ setEditing(false)
97
+ setSaveError(null)
98
+ setDraft(entry.body)
99
+ }
100
+
101
+ return (
102
+ <div
103
+ className={[
104
+ 'border rounded-lg bg-card transition-all',
105
+ editing ? 'cursor-default' : 'cursor-pointer',
106
+ expanded ? '' : 'hover:border-primary/30',
107
+ 'border-border',
108
+ ].join(' ')}
109
+ onClick={editing ? undefined : onClick}
110
+ style={expanded ? { borderColor: m.dot + '66' } : undefined}
111
+ >
112
+ <div className="px-4 py-3.5 flex items-start gap-3">
113
+ {/* Type dot */}
114
+ <div
115
+ className="flex-shrink-0 w-2 h-2 rounded-full mt-2"
116
+ style={{ backgroundColor: m.dot }}
117
+ />
118
+
119
+ <div className="flex-1 min-w-0 space-y-1.5">
120
+ {/* Header row */}
121
+ <div className="flex flex-wrap items-center gap-2">
122
+ <span className="text-sm font-mono font-semibold text-foreground truncate">{entry.name}</span>
123
+ <TypeBadge type={entry.type} />
124
+ <StaleBadge mtime={entry.mtime} />
125
+ {expanded && !editing && (
126
+ <button
127
+ onClick={handleEdit}
128
+ className="ml-auto text-[10px] font-mono px-2 py-0.5 rounded border border-border text-muted-foreground hover:border-primary/40 hover:text-foreground transition-colors"
129
+ >
130
+ edit
131
+ </button>
132
+ )}
133
+ </div>
134
+
135
+ {/* Description */}
136
+ {entry.description && (
137
+ <p className="text-xs font-mono text-muted-foreground">{entry.description}</p>
138
+ )}
139
+
140
+ {/* Body preview (collapsed) */}
141
+ {!expanded && preview && (
142
+ <p className="text-xs font-mono text-muted-foreground/60 line-clamp-2">{preview}</p>
143
+ )}
144
+
145
+ {/* Full body (expanded, read mode) */}
146
+ {expanded && !editing && (
147
+ <pre className="mt-2 text-xs font-mono text-foreground/80 whitespace-pre-wrap bg-muted/40 rounded p-3 overflow-x-auto max-h-96 overflow-y-auto">
148
+ {entry.body}
149
+ </pre>
150
+ )}
151
+
152
+ {/* Edit mode */}
153
+ {expanded && editing && (
154
+ <div className="mt-2 space-y-2" onClick={e => e.stopPropagation()}>
155
+ <textarea
156
+ className="w-full min-h-64 bg-muted/40 border border-primary/40 rounded p-3 text-xs font-mono text-foreground resize-y outline-none focus:border-primary/70 transition-colors"
157
+ value={draft}
158
+ onChange={e => setDraft(e.target.value)}
159
+ spellCheck={false}
160
+ />
161
+ {saveError && (
162
+ <p className="text-[11px] font-mono text-[#f87171]">{saveError}</p>
163
+ )}
164
+ <div className="flex gap-2">
165
+ <button
166
+ onClick={handleSave}
167
+ disabled={saving}
168
+ className="px-3 py-1.5 text-xs font-mono rounded border border-[#34d399]/50 text-[#34d399] bg-[#34d399]/10 hover:bg-[#34d399]/20 disabled:opacity-50 transition-colors"
169
+ >
170
+ {saving ? 'saving…' : 'save'}
171
+ </button>
172
+ <button
173
+ onClick={handleCancel}
174
+ disabled={saving}
175
+ className="px-3 py-1.5 text-xs font-mono rounded border border-border text-muted-foreground hover:text-foreground hover:border-primary/30 disabled:opacity-50 transition-colors"
176
+ >
177
+ cancel
178
+ </button>
179
+ </div>
180
+ </div>
181
+ )}
182
+
183
+ {/* Footer */}
184
+ <div className="flex flex-wrap items-center gap-2 pt-0.5">
185
+ <span className="text-[10px] font-mono text-muted-foreground/60 bg-muted px-1.5 py-0.5 rounded">
186
+ {projectName}
187
+ </span>
188
+ <span className="text-[10px] font-mono text-muted-foreground/40">{shortPath}</span>
189
+ <span className="text-[10px] font-mono text-muted-foreground/50 ml-auto">
190
+ {formatRelativeDate(entry.mtime)}
191
+ </span>
192
+ </div>
193
+ </div>
194
+ </div>
195
+ </div>
196
+ )
197
+ }
198
+
199
+ // ── Stat card ─────────────────────────────────────────────────────────────────
200
+
201
+ function StatCard({ value, label, color }: { value: number; label: string; color: string }) {
202
+ return (
203
+ <div className="border border-border bg-card rounded-lg px-4 py-3 flex flex-col gap-1">
204
+ <span className="text-2xl font-mono font-bold" style={{ color }}>{value}</span>
205
+ <span className="text-[10px] font-mono uppercase tracking-widest text-muted-foreground">{label}</span>
206
+ </div>
207
+ )
208
+ }
209
+
210
+ // ── Page ──────────────────────────────────────────────────────────────────────
211
+
212
+ export default function MemoryPage() {
213
+ const { data, error, isLoading } = useSWR<{ memories: MemoryEntry[] }>(
214
+ '/api/memory', fetcher, { refreshInterval: 15_000 }
215
+ )
216
+ const [filter, setFilter] = useState<FilterType>('all')
217
+ const [search, setSearch] = useState('')
218
+ const [expandedId, setExpandedId] = useState<string | null>(null)
219
+
220
+ const memories = data?.memories ?? []
221
+
222
+ const counts = useMemo(() => {
223
+ const c: Record<string, number> = { all: memories.length }
224
+ for (const type of ['user', 'feedback', 'project', 'reference', 'index']) {
225
+ c[type] = memories.filter(m => m.type === type).length
226
+ }
227
+ return c
228
+ }, [memories])
229
+
230
+ const staleCount = useMemo(
231
+ // eslint-disable-next-line react-hooks/purity
232
+ () => memories.filter(m => (Date.now() - new Date(m.mtime).getTime()) / 86_400_000 >= 30).length,
233
+ [memories]
234
+ )
235
+
236
+ const projectCount = useMemo(
237
+ () => new Set(memories.map(m => m.projectSlug)).size,
238
+ [memories]
239
+ )
240
+
241
+ const filtered = useMemo(() => {
242
+ return memories.filter(m => {
243
+ if (filter !== 'all' && m.type !== filter) return false
244
+ if (search) {
245
+ const q = search.toLowerCase()
246
+ return (
247
+ m.name.toLowerCase().includes(q) ||
248
+ m.description.toLowerCase().includes(q) ||
249
+ m.body.toLowerCase().includes(q) ||
250
+ m.projectPath.toLowerCase().includes(q)
251
+ )
252
+ }
253
+ return true
254
+ })
255
+ }, [memories, filter, search])
256
+
257
+ function toggleExpand(id: string) {
258
+ setExpandedId(prev => (prev === id ? null : id))
259
+ }
260
+
261
+ return (
262
+ <div className="flex flex-col min-h-screen">
263
+ <TopBar title="orca-pulse · memory" subtitle="~/.orca/projects/*/memory/" />
264
+ <div className="p-4 md:p-6 space-y-5">
265
+
266
+ {error && <p className="text-[#f87171] text-sm font-mono">Error loading memories.</p>}
267
+
268
+ {isLoading && (
269
+ <div className="space-y-3">
270
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
271
+ {Array.from({ length: 4 }).map((_, i) => (
272
+ <div key={i} className="h-20 bg-muted rounded-lg animate-pulse" />
273
+ ))}
274
+ </div>
275
+ {Array.from({ length: 6 }).map((_, i) => (
276
+ <div key={i} className="h-24 bg-muted rounded-lg animate-pulse" />
277
+ ))}
278
+ </div>
279
+ )}
280
+
281
+ {data && (
282
+ <>
283
+ {/* Stat cards */}
284
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
285
+ <StatCard value={memories.length} label="total memories" color="#fbbf24" />
286
+ <StatCard value={projectCount} label="projects" color="var(--viz-sky)" />
287
+ <StatCard value={counts.feedback ?? 0} label="feedback" color="#f87171" />
288
+ <StatCard value={staleCount} label="stale (>30d)" color="#94a3b8" />
289
+ </div>
290
+
291
+ {/* Type filter tabs */}
292
+ <div className="flex flex-wrap gap-2">
293
+ {FILTER_TYPES.map(type => {
294
+ const m = type === 'all' ? null : TYPE_META[type as MemoryType]
295
+ const count = counts[type] ?? 0
296
+ const active = filter === type
297
+ return (
298
+ <button
299
+ key={type}
300
+ onClick={() => setFilter(type)}
301
+ className={[
302
+ 'flex items-center gap-1.5 px-3 py-1.5 rounded border text-xs font-mono transition-all',
303
+ active
304
+ ? 'border-primary/50 bg-primary/10 text-primary'
305
+ : 'border-border bg-card text-muted-foreground hover:border-primary/30 hover:text-foreground',
306
+ ].join(' ')}
307
+ >
308
+ {m && (
309
+ <span className="w-1.5 h-1.5 rounded-full flex-shrink-0" style={{ backgroundColor: m.dot }} />
310
+ )}
311
+ {type}
312
+ <span className="opacity-60">({count})</span>
313
+ </button>
314
+ )
315
+ })}
316
+ </div>
317
+
318
+ {/* Search */}
319
+ <div className="border border-border rounded-lg bg-card focus-within:border-primary/40 transition-colors">
320
+ <input
321
+ className="w-full bg-transparent px-4 py-2.5 text-sm font-mono text-foreground placeholder-muted-foreground/50 outline-none"
322
+ placeholder="search memories..."
323
+ value={search}
324
+ onChange={e => setSearch(e.target.value)}
325
+ />
326
+ </div>
327
+
328
+ {/* Result count */}
329
+ {(search || filter !== 'all') && (
330
+ <p className="text-xs font-mono text-muted-foreground/60">
331
+ showing <span className="text-[#fbbf24]">{filtered.length}</span> of {memories.length} memories
332
+ </p>
333
+ )}
334
+
335
+ {/* Memory list */}
336
+ {filtered.length === 0 ? (
337
+ <div className="text-center py-16">
338
+ <p className="text-3xl mb-3">🧠</p>
339
+ <p className="text-muted-foreground/60 text-sm font-mono">
340
+ {memories.length === 0
341
+ ? 'No memory files found in ~/.orca/projects/*/memory/'
342
+ : 'No memories match your filter.'}
343
+ </p>
344
+ </div>
345
+ ) : (
346
+ <div className="space-y-2">
347
+ {filtered.map(entry => {
348
+ const id = `${entry.projectSlug}/${entry.file}`
349
+ return (
350
+ <MemoryCard
351
+ key={id}
352
+ entry={entry}
353
+ expanded={expandedId === id}
354
+ onClick={() => toggleExpand(id)}
355
+ />
356
+ )
357
+ })}
358
+ </div>
359
+ )}
360
+ </>
361
+ )}
362
+ </div>
363
+ </div>
364
+ )
365
+ }