@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,308 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import useSWR from 'swr'
5
+ import { TopBar } from '@/components/layout/top-bar'
6
+ import { cn } from '@/lib/utils'
7
+ import { ChevronDown, ClipboardList } from 'lucide-react'
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
+ interface PlanFile {
13
+ name: string
14
+ content: string
15
+ mtime: string
16
+ }
17
+
18
+ function formatDate(iso: string) {
19
+ return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
20
+ }
21
+
22
+ function wordCount(text: string) {
23
+ return text.trim().split(/\s+/).filter(Boolean).length
24
+ }
25
+
26
+ function extractTitle(content: string): string | null {
27
+ const firstLine = content.split('\n').find(l => l.trim())
28
+ if (firstLine?.startsWith('#')) return firstLine.replace(/^#+\s*/, '').trim()
29
+ return null
30
+ }
31
+
32
+ // Minimal markdown renderer — handles headings, bold, code, lists, hr, plain text
33
+ function MarkdownLine({ line }: { line: string }) {
34
+ // h1
35
+ if (/^#\s/.test(line))
36
+ return (
37
+ <p className="text-foreground font-bold text-base font-mono mt-4 mb-1 first:mt-0 leading-snug tracking-tight">
38
+ {line.replace(/^#\s/, '')}
39
+ </p>
40
+ )
41
+ // h2
42
+ if (/^##\s/.test(line))
43
+ return (
44
+ <p className="text-primary font-bold text-sm font-mono mt-3 mb-1 leading-snug">
45
+ {line.replace(/^##\s/, '')}
46
+ </p>
47
+ )
48
+ // h3
49
+ if (/^###\s/.test(line))
50
+ return (
51
+ <p className="text-foreground font-semibold text-sm font-mono mt-2 mb-0.5 leading-snug">
52
+ {line.replace(/^###\s/, '')}
53
+ </p>
54
+ )
55
+ // hr
56
+ if (/^---+$/.test(line.trim()))
57
+ return <hr className="border-border my-3" />
58
+ // empty
59
+ if (!line.trim())
60
+ return <div className="h-2" />
61
+ // bullet list
62
+ if (/^[-*]\s/.test(line))
63
+ return (
64
+ <p className="text-foreground/95 text-sm font-mono leading-relaxed flex gap-2">
65
+ <span className="text-primary flex-shrink-0">›</span>
66
+ <InlineMarkdown text={line.replace(/^[-*]\s/, '')} />
67
+ </p>
68
+ )
69
+ // numbered list
70
+ if (/^\d+\.\s/.test(line)) {
71
+ const [num, ...rest] = line.split(/\.\s(.+)/)
72
+ return (
73
+ <p className="text-foreground/95 text-sm font-mono leading-relaxed flex gap-2">
74
+ <span className="text-primary flex-shrink-0 w-5 text-right">{num}.</span>
75
+ <InlineMarkdown text={rest.join('. ')} />
76
+ </p>
77
+ )
78
+ }
79
+ // code block marker — handled at block level, fallback
80
+ if (line.startsWith('```'))
81
+ return <span className="text-muted-foreground/60 text-xs font-mono">{line}</span>
82
+
83
+ return (
84
+ <p className="text-foreground/95 text-sm font-mono leading-relaxed">
85
+ <InlineMarkdown text={line} />
86
+ </p>
87
+ )
88
+ }
89
+
90
+ function InlineMarkdown({ text }: { text: string }) {
91
+ // Bold + italic: ***text***
92
+ // Bold: **text**
93
+ // Inline code: `text`
94
+ const parts = text.split(/(`[^`]+`|\*\*\*[^*]+\*\*\*|\*\*[^*]+\*\*|\*[^*]+\*)/)
95
+ return (
96
+ <>
97
+ {parts.map((p, i) => {
98
+ if (p.startsWith('***') && p.endsWith('***'))
99
+ return <strong key={i} className="text-primary italic">{p.slice(3, -3)}</strong>
100
+ if (p.startsWith('**') && p.endsWith('**'))
101
+ return <strong key={i} className="text-foreground font-bold">{p.slice(2, -2)}</strong>
102
+ if (p.startsWith('*') && p.endsWith('*'))
103
+ return <em key={i} className="text-muted-foreground italic">{p.slice(1, -1)}</em>
104
+ if (p.startsWith('`') && p.endsWith('`'))
105
+ return (
106
+ <code
107
+ key={i}
108
+ className="text-emerald-800 dark:text-emerald-300 bg-emerald-950/10 dark:bg-emerald-950/40 px-1 py-0.5 rounded text-xs font-mono"
109
+ >
110
+ {p.slice(1, -1)}
111
+ </code>
112
+ )
113
+ return <span key={i}>{p}</span>
114
+ })}
115
+ </>
116
+ )
117
+ }
118
+
119
+ function MarkdownContent({ content }: { content: string }) {
120
+ const lines = content.split('\n')
121
+ const result: React.ReactNode[] = []
122
+ let inCodeBlock = false
123
+ let codeLines: string[] = []
124
+ let codeLang = ''
125
+
126
+ for (let i = 0; i < lines.length; i++) {
127
+ const line = lines[i]
128
+ if (line.startsWith('```')) {
129
+ if (!inCodeBlock) {
130
+ inCodeBlock = true
131
+ codeLang = line.slice(3).trim()
132
+ codeLines = []
133
+ } else {
134
+ inCodeBlock = false
135
+ result.push(
136
+ <div
137
+ key={i}
138
+ className="my-2 rounded-lg border border-border bg-muted/80 dark:bg-muted overflow-x-auto shadow-sm"
139
+ >
140
+ {codeLang && (
141
+ <div className="px-3 py-1.5 border-b border-border text-[10px] font-mono text-muted-foreground uppercase tracking-wide">
142
+ {codeLang}
143
+ </div>
144
+ )}
145
+ <pre className="px-3 py-2.5 text-xs font-mono text-foreground leading-relaxed">{codeLines.join('\n')}</pre>
146
+ </div>
147
+ )
148
+ codeLines = []
149
+ codeLang = ''
150
+ }
151
+ continue
152
+ }
153
+ if (inCodeBlock) {
154
+ codeLines.push(line)
155
+ continue
156
+ }
157
+ result.push(<MarkdownLine key={i} line={line} />)
158
+ }
159
+
160
+ return <div className="space-y-0.5">{result}</div>
161
+ }
162
+
163
+ function PlanCard({ plan }: { plan: PlanFile }) {
164
+ const [expanded, setExpanded] = useState(false)
165
+ const lines = plan.content.split('\n').filter(l => l.trim())
166
+ const title = extractTitle(plan.content)
167
+ const words = wordCount(plan.content)
168
+
169
+ // Preview: first 8 non-empty lines
170
+ const previewContent = plan.content.split('\n').slice(0, 12).join('\n')
171
+ const hasMore = plan.content.split('\n').length > 12
172
+
173
+ return (
174
+ <div
175
+ className={[
176
+ 'border rounded-xl transition-all duration-200 shadow-sm',
177
+ expanded ? 'border-primary/40 bg-card ring-1 ring-primary/10' : 'border-border bg-card hover:border-primary/25',
178
+ ].join(' ')}
179
+ >
180
+
181
+ {/* Header — always visible */}
182
+ <button
183
+ type="button"
184
+ aria-expanded={expanded}
185
+ className="flex w-full items-start justify-between gap-4 rounded-t-xl px-5 py-4 text-left md:px-6"
186
+ onClick={() => setExpanded(e => !e)}
187
+ >
188
+ <div className="min-w-0 flex-1">
189
+ <div className="mb-2 flex items-center gap-2.5">
190
+ <ClipboardList className="h-4 w-4 shrink-0 text-[#d97706]" aria-hidden />
191
+ <span className="truncate font-mono text-sm font-bold text-foreground">
192
+ {title ?? plan.name}
193
+ </span>
194
+ </div>
195
+ {title && title !== plan.name && (
196
+ <p className="mb-2 pl-7 font-mono text-xs text-muted-foreground/60">{plan.name}</p>
197
+ )}
198
+ <div className="flex flex-wrap items-center gap-4 pl-7">
199
+ <span className="font-mono text-xs text-muted-foreground/60">{formatDate(plan.mtime)}</span>
200
+ <span className="font-mono text-xs text-muted-foreground/60">{lines.length} lines</span>
201
+ <span className="font-mono text-xs text-muted-foreground/60">{words} words</span>
202
+ </div>
203
+ </div>
204
+ <ChevronDown
205
+ className={cn(
206
+ 'mt-0.5 h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200',
207
+ expanded && 'rotate-180'
208
+ )}
209
+ aria-hidden
210
+ />
211
+ </button>
212
+
213
+ {/* Preview (collapsed) */}
214
+ {!expanded && (
215
+ <div className="px-5 pb-5 md:px-6 border-t border-border/60 bg-muted/25 dark:bg-muted/10">
216
+ <div className="pt-4 rounded-lg">
217
+ <MarkdownContent content={previewContent} />
218
+ {hasMore && (
219
+ <p className="text-muted-foreground text-xs font-mono mt-3 pl-0.5">
220
+ — {plan.content.split('\n').length - 12} more lines, click to expand —
221
+ </p>
222
+ )}
223
+ </div>
224
+ </div>
225
+ )}
226
+
227
+ {/* Full content (expanded) */}
228
+ {expanded && (
229
+ <div className="px-5 pb-6 md:px-6 border-t border-primary/15 bg-muted/25 dark:bg-muted/10">
230
+ <div className="pt-5 max-h-[min(70vh,56rem)] overflow-y-auto rounded-lg pr-1">
231
+ <MarkdownContent content={plan.content} />
232
+ </div>
233
+ </div>
234
+ )}
235
+ </div>
236
+ )
237
+ }
238
+
239
+ export default function PlansPage() {
240
+ const { data, error, isLoading } = useSWR<{ plans: PlanFile[] }>(
241
+ '/api/plans', fetcher, { refreshInterval: 30_000 }
242
+ )
243
+ const [search, setSearch] = useState('')
244
+
245
+ const plans = data?.plans ?? []
246
+ const filtered = plans.filter(p =>
247
+ !search ||
248
+ p.name.toLowerCase().includes(search.toLowerCase()) ||
249
+ p.content.toLowerCase().includes(search.toLowerCase())
250
+ )
251
+
252
+ return (
253
+ <div className="flex flex-col min-h-screen">
254
+ <TopBar title="orca-pulse · plans" subtitle="~/.orca/plans/" />
255
+ <div className="p-4 md:p-6 space-y-5">
256
+
257
+ {error && <p className="text-[#f87171] text-sm font-mono">Error: {String(error)}</p>}
258
+
259
+ {isLoading && (
260
+ <div className="space-y-3">
261
+ {Array.from({ length: 4 }).map((_, i) => (
262
+ <div key={i} className="h-28 bg-muted rounded-lg animate-pulse" />
263
+ ))}
264
+ </div>
265
+ )}
266
+
267
+ {data && (
268
+ <>
269
+ {/* Search + count */}
270
+ <div className="flex flex-col sm:flex-row gap-3 items-start sm:items-center">
271
+ <div className="flex-1 border border-border rounded-lg bg-card w-full focus-within:border-primary/40 transition-colors">
272
+ <input
273
+ className="w-full bg-transparent px-4 py-2.5 text-sm font-mono text-foreground placeholder-muted-foreground/50 outline-none"
274
+ placeholder="search plans by name or content..."
275
+ value={search}
276
+ onChange={e => setSearch(e.target.value)}
277
+ />
278
+ </div>
279
+ <p className="text-sm font-mono text-muted-foreground whitespace-nowrap">
280
+ <span className="text-[#fbbf24] font-bold">{filtered.length}</span>
281
+ {filtered.length !== plans.length && (
282
+ <span className="text-muted-foreground/60"> of {plans.length}</span>
283
+ )} plans
284
+ </p>
285
+ </div>
286
+
287
+ {filtered.length === 0 ? (
288
+ <div className="text-center py-16">
289
+ <p className="text-[#d97706] text-2xl mb-3">📋</p>
290
+ <p className="text-muted-foreground/60 text-sm font-mono">
291
+ {plans.length === 0
292
+ ? 'No plans found in ~/.orca/plans/'
293
+ : 'No plans match your search.'}
294
+ </p>
295
+ </div>
296
+ ) : (
297
+ <div className="space-y-3">
298
+ {filtered.map(plan => (
299
+ <PlanCard key={plan.name} plan={plan} />
300
+ ))}
301
+ </div>
302
+ )}
303
+ </>
304
+ )}
305
+ </div>
306
+ </div>
307
+ )
308
+ }