@axplusb/kepler 0.0.1 → 1.0.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.
- package/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +98 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- 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
|
+
}
|