@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.
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 +98 -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,94 @@
1
+ 'use client'
2
+
3
+ import ReactMarkdown from 'react-markdown'
4
+ import remarkGfm from 'remark-gfm'
5
+ import { cn } from '@/lib/utils'
6
+
7
+ interface Props {
8
+ content: string
9
+ className?: string
10
+ }
11
+
12
+ /**
13
+ * Renders Claude assistant text as GitHub-flavored markdown with app-consistent styling.
14
+ */
15
+ export function AssistantMarkdown({ content, className }: Props) {
16
+ return (
17
+ <div className={cn('assistant-md text-sm text-foreground/90', className)}>
18
+ <ReactMarkdown
19
+ remarkPlugins={[remarkGfm]}
20
+ components={{
21
+ h1: ({ children }) => (
22
+ <h1 className="mt-4 mb-2 border-b border-border pb-1 text-lg font-bold first:mt-0">{children}</h1>
23
+ ),
24
+ h2: ({ children }) => (
25
+ <h2 className="mt-4 mb-2 text-base font-semibold first:mt-0">{children}</h2>
26
+ ),
27
+ h3: ({ children }) => (
28
+ <h3 className="mt-3 mb-1.5 text-sm font-semibold first:mt-0">{children}</h3>
29
+ ),
30
+ h4: ({ children }) => (
31
+ <h4 className="mt-2 mb-1 text-sm font-medium first:mt-0">{children}</h4>
32
+ ),
33
+ p: ({ children }) => <p className="my-2 leading-relaxed first:mt-0 last:mb-0">{children}</p>,
34
+ ul: ({ children }) => (
35
+ <ul className="my-2 list-disc space-y-1 pl-5 marker:text-muted-foreground">{children}</ul>
36
+ ),
37
+ ol: ({ children }) => (
38
+ <ol className="my-2 list-decimal space-y-1 pl-5 marker:text-muted-foreground">{children}</ol>
39
+ ),
40
+ li: ({ children }) => <li className="leading-relaxed [&>p]:my-0">{children}</li>,
41
+ blockquote: ({ children }) => (
42
+ <blockquote className="my-2 border-l-2 border-primary/40 pl-3 text-muted-foreground">{children}</blockquote>
43
+ ),
44
+ a: ({ href, children }) => (
45
+ <a
46
+ href={href}
47
+ target="_blank"
48
+ rel="noopener noreferrer"
49
+ className="font-medium text-primary underline decoration-primary/40 underline-offset-2 hover:decoration-primary"
50
+ >
51
+ {children}
52
+ </a>
53
+ ),
54
+ strong: ({ children }) => <strong className="font-semibold text-foreground">{children}</strong>,
55
+ em: ({ children }) => <em className="italic text-muted-foreground">{children}</em>,
56
+ hr: () => <hr className="my-4 border-border" />,
57
+ table: ({ children }) => (
58
+ <div className="my-3 overflow-x-auto rounded-md border border-border">
59
+ <table className="w-full min-w-[16rem] border-collapse text-[13px]">{children}</table>
60
+ </div>
61
+ ),
62
+ thead: ({ children }) => <thead className="bg-muted/50">{children}</thead>,
63
+ th: ({ children }) => (
64
+ <th className="border-b border-border px-2 py-1.5 text-left font-semibold">{children}</th>
65
+ ),
66
+ td: ({ children }) => <td className="border-b border-border/80 px-2 py-1.5 align-top">{children}</td>,
67
+ tr: ({ children }) => <tr className="border-border/60">{children}</tr>,
68
+ code: ({ className, children }) => {
69
+ const isBlock = /language-/.test(className ?? '')
70
+ if (isBlock) {
71
+ return (
72
+ <code className={cn('font-mono text-[13px] leading-relaxed text-foreground', className)}>
73
+ {children}
74
+ </code>
75
+ )
76
+ }
77
+ return (
78
+ <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-[12px] text-foreground before:content-none after:content-none">
79
+ {children}
80
+ </code>
81
+ )
82
+ },
83
+ pre: ({ children }) => (
84
+ <pre className="my-3 overflow-x-auto rounded-lg border border-border bg-muted/60 p-3 font-mono text-[13px] leading-relaxed [&>code]:bg-transparent [&>code]:p-0">
85
+ {children}
86
+ </pre>
87
+ ),
88
+ }}
89
+ >
90
+ {content}
91
+ </ReactMarkdown>
92
+ </div>
93
+ )
94
+ }
@@ -0,0 +1,25 @@
1
+ import type { CompactionEvent } from '@/types/claude'
2
+ import { formatTokens } from '@/lib/decode'
3
+
4
+ export function CompactionCard({ event }: { event: CompactionEvent }) {
5
+ return (
6
+ <div className="my-3 border border-amber-500/40 bg-amber-500/10 rounded-lg px-4 py-3">
7
+ <div className="flex items-center gap-2 text-amber-400 text-sm font-bold mb-1">
8
+ <span>⚡</span>
9
+ <span>CONTEXT COMPACTION</span>
10
+ <span className="ml-auto text-amber-500/70 font-normal">
11
+ {new Date(event.timestamp).toLocaleTimeString()}
12
+ </span>
13
+ </div>
14
+ <div className="flex gap-4 text-sm text-amber-300/80">
15
+ <span>trigger: <span className="text-amber-300 font-medium">{event.trigger}</span></span>
16
+ <span>context before: <span className="text-amber-300 font-medium">{formatTokens(event.pre_tokens)} tokens</span></span>
17
+ </div>
18
+ {event.summary && (
19
+ <p className="mt-1.5 text-sm text-amber-200/60 italic line-clamp-2">
20
+ &ldquo;{event.summary}&rdquo;
21
+ </p>
22
+ )}
23
+ </div>
24
+ )
25
+ }
@@ -0,0 +1,231 @@
1
+ 'use client'
2
+
3
+ import { formatCost, formatTokens, formatDuration, projectDisplayName } from '@/lib/decode'
4
+ import type { ReplayData, SessionMeta } from '@/types/claude'
5
+ import { Badge } from '@/components/ui/badge'
6
+ import { Separator } from '@/components/ui/separator'
7
+ import { GitBranch, Clock, FileCode2, Zap, Cpu } from 'lucide-react'
8
+
9
+ interface Props {
10
+ replay: ReplayData
11
+ meta?: SessionMeta
12
+ }
13
+
14
+ function SectionTitle({ children }: { children: React.ReactNode }) {
15
+ return (
16
+ <h3 className="mb-3 text-xs font-semibold uppercase tracking-widest text-muted-foreground">
17
+ {children}
18
+ </h3>
19
+ )
20
+ }
21
+
22
+ export function SessionSidebar({ replay, meta }: Props) {
23
+ let totalInput = 0,
24
+ totalOutput = 0,
25
+ totalCacheWrite = 0,
26
+ totalCacheRead = 0
27
+ for (const t of replay.turns) {
28
+ if (t.usage) {
29
+ totalInput += t.usage.input_tokens ?? 0
30
+ totalOutput += t.usage.output_tokens ?? 0
31
+ totalCacheWrite += t.usage.cache_creation_input_tokens ?? 0
32
+ totalCacheRead += t.usage.cache_read_input_tokens ?? 0
33
+ }
34
+ }
35
+ const totalTokens = totalInput + totalOutput + totalCacheWrite + totalCacheRead
36
+ const pct = (n: number) => (totalTokens > 0 ? (n / totalTokens) * 100 : 0)
37
+
38
+ const toolCounts = new Map<string, number>()
39
+ for (const t of replay.turns) {
40
+ for (const tc of t.tool_calls ?? []) {
41
+ toolCounts.set(tc.name, (toolCounts.get(tc.name) ?? 0) + 1)
42
+ }
43
+ }
44
+ const topTools = [...toolCounts.entries()]
45
+ .sort((a, b) => b[1] - a[1])
46
+ .slice(0, 8)
47
+ const maxToolCount = topTools[0]?.[1] ?? 1
48
+
49
+ const assistantTurns = replay.turns.filter(t => t.type === 'assistant')
50
+
51
+ const tokenBreakdown = [
52
+ { label: 'Input', val: totalInput, color: 'var(--viz-sky)', bg: 'bg-blue-700 dark:bg-blue-400' },
53
+ { label: 'Output', val: totalOutput, color: '#d97706', bg: 'bg-amber-500' },
54
+ { label: 'Cache Write', val: totalCacheWrite, color: '#a78bfa', bg: 'bg-violet-400' },
55
+ { label: 'Cache Read', val: totalCacheRead, color: '#34d399', bg: 'bg-emerald-400' },
56
+ ]
57
+
58
+ const showTools = topTools.length > 0
59
+ const showCompactions = replay.compactions.length > 0
60
+
61
+ return (
62
+ <div className="text-sm">
63
+ {/* Token breakdown */}
64
+ <section>
65
+ <SectionTitle>
66
+ <span className="inline-flex items-center gap-1.5">
67
+ <Cpu className="h-3.5 w-3.5" /> Token breakdown
68
+ </span>
69
+ </SectionTitle>
70
+ <div className="space-y-3">
71
+ {tokenBreakdown.map(({ label, val, color, bg }) => (
72
+ <div key={label} className="space-y-1">
73
+ <div className="flex items-center justify-between">
74
+ <span className="text-xs text-muted-foreground">{label}</span>
75
+ <span className="font-mono text-xs font-semibold" style={{ color }}>
76
+ {formatTokens(val)}
77
+ </span>
78
+ </div>
79
+ <div className="h-1.5 overflow-hidden rounded-full bg-muted">
80
+ <div
81
+ className={`h-full rounded-full ${bg} opacity-70 transition-all`}
82
+ style={{ width: `${Math.max(2, pct(val))}%` }}
83
+ />
84
+ </div>
85
+ </div>
86
+ ))}
87
+ <div className="flex items-center justify-between border-t border-border/50 pt-3">
88
+ <span className="text-xs font-semibold text-muted-foreground">Total</span>
89
+ <div className="flex items-center gap-2">
90
+ <span className="font-mono text-xs font-bold text-foreground">{formatTokens(totalTokens)}</span>
91
+ <span className="font-mono text-xs font-bold text-[#d97706]">{formatCost(replay.total_cost)}</span>
92
+ </div>
93
+ </div>
94
+ </div>
95
+ </section>
96
+
97
+ {showTools && (
98
+ <>
99
+ <Separator className="my-5" />
100
+ <section>
101
+ <SectionTitle>Tools used</SectionTitle>
102
+ <div className="space-y-2">
103
+ {topTools.map(([name, count]) => {
104
+ const shortName = name.startsWith('mcp__') ? name.split('__').slice(1).join(' · ') : name
105
+ const width = Math.round((count / maxToolCount) * 100)
106
+ return (
107
+ <div key={name} className="flex items-center gap-2">
108
+ <span className="w-24 truncate text-xs text-muted-foreground" title={name}>
109
+ {shortName}
110
+ </span>
111
+ <div className="h-1.5 flex-1 overflow-hidden rounded-full bg-muted">
112
+ <div className="h-full rounded-full bg-[#d97706]/60" style={{ width: `${width}%` }} />
113
+ </div>
114
+ <span className="w-5 text-right text-xs tabular-nums text-muted-foreground/60">{count}</span>
115
+ </div>
116
+ )
117
+ })}
118
+ </div>
119
+ </section>
120
+ </>
121
+ )}
122
+
123
+ {showCompactions && (
124
+ <>
125
+ <Separator className="my-5" />
126
+ <section>
127
+ <SectionTitle>
128
+ <span className="inline-flex items-center gap-1.5">
129
+ <Zap className="h-3.5 w-3.5 text-amber-500" /> Compactions
130
+ </span>
131
+ </SectionTitle>
132
+ <div className="space-y-2.5">
133
+ {replay.compactions.map(c => (
134
+ <div
135
+ key={c.uuid}
136
+ className="flex items-start gap-2 rounded-lg border border-amber-500/15 bg-amber-500/5 px-2.5 py-2"
137
+ >
138
+ <Zap className="mt-0.5 h-3 w-3 shrink-0 text-amber-400" />
139
+ <div className="space-y-0.5">
140
+ <div className="flex items-center gap-1.5">
141
+ <span className="text-xs font-medium text-amber-300/80">Turn {c.turn_index}</span>
142
+ <Badge
143
+ variant="outline"
144
+ className="h-4 border-amber-500/30 px-1 py-0 text-[11px] text-amber-400/70"
145
+ >
146
+ {c.trigger}
147
+ </Badge>
148
+ </div>
149
+ <span className="text-xs text-muted-foreground/60">{formatTokens(c.pre_tokens)} tok before</span>
150
+ </div>
151
+ </div>
152
+ ))}
153
+ </div>
154
+ </section>
155
+ </>
156
+ )}
157
+
158
+ <Separator className="my-5" />
159
+
160
+ <section>
161
+ <SectionTitle>Session info</SectionTitle>
162
+ <div className="space-y-2">
163
+ {replay.slug && (
164
+ <div className="flex items-start gap-2">
165
+ <span className="w-16 shrink-0 text-xs text-muted-foreground/50">Slug</span>
166
+ <span className="break-all text-xs text-foreground/80">{replay.slug}</span>
167
+ </div>
168
+ )}
169
+ {replay.version && (
170
+ <div className="flex items-center gap-2">
171
+ <span className="w-16 shrink-0 text-xs text-muted-foreground/50">Version</span>
172
+ <Badge variant="outline" className="h-4 px-1.5 py-0 font-mono text-[11px]">
173
+ v{replay.version}
174
+ </Badge>
175
+ </div>
176
+ )}
177
+ {replay.git_branch && (
178
+ <div className="flex items-center gap-2">
179
+ <span className="w-16 shrink-0 text-xs text-muted-foreground/50">Branch</span>
180
+ <div className="flex min-w-0 items-center gap-1">
181
+ <GitBranch className="h-3 w-3 shrink-0 text-muted-foreground/40" />
182
+ <span className="truncate font-mono text-xs text-foreground/70">{replay.git_branch}</span>
183
+ </div>
184
+ </div>
185
+ )}
186
+ <div className="flex items-center gap-2">
187
+ <span className="w-16 shrink-0 text-xs text-muted-foreground/50">Turns</span>
188
+ <span className="text-xs font-semibold text-foreground/80">{assistantTurns.length}</span>
189
+ </div>
190
+ {meta && (
191
+ <>
192
+ {meta.duration_minutes > 0 && (
193
+ <div className="flex items-center gap-2">
194
+ <span className="w-16 shrink-0 text-xs text-muted-foreground/50">Duration</span>
195
+ <span className="flex items-center gap-1 text-xs text-foreground/80">
196
+ <Clock className="h-3 w-3 text-muted-foreground/40" />
197
+ {formatDuration(meta.duration_minutes)}
198
+ </span>
199
+ </div>
200
+ )}
201
+ {meta.project_path && (
202
+ <div className="flex items-start gap-2">
203
+ <span className="w-16 shrink-0 text-xs text-muted-foreground/50">Project</span>
204
+ <span className="truncate text-xs text-foreground/70">{projectDisplayName(meta.project_path)}</span>
205
+ </div>
206
+ )}
207
+ {(meta.lines_added ?? 0) > 0 && (
208
+ <div className="flex items-center gap-2">
209
+ <span className="w-16 shrink-0 text-xs text-muted-foreground/50">Lines</span>
210
+ <div className="flex items-center gap-1.5">
211
+ <span className="font-mono text-xs text-emerald-400">+{meta.lines_added}</span>
212
+ <span className="font-mono text-xs text-red-400">-{meta.lines_removed}</span>
213
+ </div>
214
+ </div>
215
+ )}
216
+ {meta.files_modified > 0 && (
217
+ <div className="flex items-center gap-2">
218
+ <span className="w-16 shrink-0 text-xs text-muted-foreground/50">Files</span>
219
+ <span className="flex items-center gap-1 text-xs text-foreground/80">
220
+ <FileCode2 className="h-3 w-3 text-muted-foreground/40" />
221
+ {meta.files_modified} modified
222
+ </span>
223
+ </div>
224
+ )}
225
+ </>
226
+ )}
227
+ </div>
228
+ </section>
229
+ </div>
230
+ )
231
+ }
@@ -0,0 +1,98 @@
1
+ 'use client'
2
+
3
+ import { useMemo } from 'react'
4
+ import {
5
+ LineChart,
6
+ Line,
7
+ XAxis,
8
+ YAxis,
9
+ Tooltip,
10
+ CartesianGrid,
11
+ ReferenceLine,
12
+ ResponsiveContainer,
13
+ } from 'recharts'
14
+ import { formatTokens, formatCost } from '@/lib/decode'
15
+ import type { ReplayTurn, CompactionEvent } from '@/types/claude'
16
+
17
+ interface Props {
18
+ turns: ReplayTurn[]
19
+ compactions: CompactionEvent[]
20
+ }
21
+
22
+ export function TokenAccumulationChart({ turns, compactions }: Props) {
23
+ const data = useMemo(() => {
24
+ const points: Array<{ turn: number; tokens: number; cost: number; label: string }> = []
25
+ let cumCost = 0
26
+ let cumTokens = 0
27
+ let turnIdx = 0
28
+
29
+ for (const t of turns) {
30
+ turnIdx++
31
+ if (t.type === 'assistant' && t.usage) {
32
+ const u = t.usage
33
+ cumTokens = (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0)
34
+ cumCost += t.estimated_cost ?? 0
35
+ points.push({ turn: turnIdx, tokens: cumTokens, cost: cumCost, label: `Turn ${turnIdx}` })
36
+ }
37
+ }
38
+ return points
39
+ }, [turns])
40
+
41
+ const compactionTurnIndices = useMemo(
42
+ () => compactions.map(c => c.turn_index),
43
+ [compactions]
44
+ )
45
+
46
+ if (data.length === 0) return null
47
+
48
+ return (
49
+ <div className="border border-border rounded bg-card p-4">
50
+ <h3 className="text-sm font-bold text-muted-foreground uppercase tracking-widest mb-3">
51
+ 📈 Token Accumulation per Turn
52
+ </h3>
53
+ <ResponsiveContainer width="100%" height={180}>
54
+ <LineChart data={data} margin={{ top: 4, right: 8, bottom: 4, left: 0 }}>
55
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
56
+ <XAxis
57
+ dataKey="turn"
58
+ tick={{ fontSize: 9, fill: 'var(--muted-foreground)' }}
59
+ tickLine={false}
60
+ axisLine={false}
61
+ label={{ value: 'Turn', position: 'insideBottom', offset: -2, fontSize: 9, fill: 'var(--muted-foreground)' }}
62
+ />
63
+ <YAxis
64
+ tick={{ fontSize: 9, fill: 'var(--muted-foreground)' }}
65
+ tickLine={false}
66
+ axisLine={false}
67
+ tickFormatter={formatTokens}
68
+ width={48}
69
+ />
70
+ <Tooltip
71
+ contentStyle={{ background: 'var(--card)', border: '1px solid var(--border)', borderRadius: 4, fontSize: 12 }}
72
+ formatter={(val: number | undefined, name?: string) => [
73
+ name === 'tokens' ? formatTokens(val ?? 0) : formatCost(val ?? 0),
74
+ name === 'tokens' ? 'Context tokens' : 'Cumulative cost',
75
+ ]}
76
+ />
77
+ {compactionTurnIndices.map(idx => (
78
+ <ReferenceLine
79
+ key={idx}
80
+ x={idx}
81
+ stroke="#f59e0b"
82
+ strokeDasharray="4 2"
83
+ label={{ value: '⚡', position: 'top', fontSize: 12 }}
84
+ />
85
+ ))}
86
+ <Line
87
+ type="monotone"
88
+ dataKey="tokens"
89
+ stroke="var(--viz-sky)"
90
+ strokeWidth={1.5}
91
+ dot={false}
92
+ activeDot={{ r: 3 }}
93
+ />
94
+ </LineChart>
95
+ </ResponsiveContainer>
96
+ </div>
97
+ )
98
+ }
@@ -0,0 +1,127 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { categoryColorMix, parseMcpTool, isMcpTool, toolBarColor } from '@/lib/tool-categories'
5
+ import type { ToolCall } from '@/types/claude'
6
+ import { Button } from '@/components/ui/button'
7
+ import { cn } from '@/lib/utils'
8
+ import {
9
+ Wrench,
10
+ Search,
11
+ Globe,
12
+ ClipboardList,
13
+ CheckCircle2,
14
+ ListTodo,
15
+ Plug,
16
+ Bot,
17
+ ChevronDown,
18
+ type LucideIcon,
19
+ } from 'lucide-react'
20
+
21
+ function truncate(s: string, n = 80): string {
22
+ return s.length > n ? s.slice(0, n) + '…' : s
23
+ }
24
+
25
+ function getToolArg(tool: ToolCall): string {
26
+ const inp = tool.input ?? {}
27
+ if (inp.command) return String(inp.command).slice(0, 60)
28
+ if (inp.file_path) return String(inp.file_path).split('/').slice(-2).join('/')
29
+ if (inp.path) return String(inp.path).split('/').slice(-2).join('/')
30
+ if (inp.pattern) return String(inp.pattern).slice(0, 60)
31
+ if (inp.query) return String(inp.query).slice(0, 60)
32
+ if (inp.url) return String(inp.url).slice(0, 60)
33
+ if (inp.description) return String(inp.description).slice(0, 60)
34
+ const keys = Object.keys(inp)
35
+ if (keys.length > 0) return truncate(String(inp[keys[0]]))
36
+ return ''
37
+ }
38
+
39
+ function getToolIcon(name: string): LucideIcon {
40
+ if (name === 'Task') return Bot
41
+ if (name === 'WebSearch') return Search
42
+ if (name === 'WebFetch') return Globe
43
+ if (name === 'EnterPlanMode') return ClipboardList
44
+ if (name === 'ExitPlanMode') return CheckCircle2
45
+ if (name === 'TodoWrite') return ListTodo
46
+ if (isMcpTool(name)) return Plug
47
+ return Wrench
48
+ }
49
+
50
+ export function ToolCallBadge({ tool, result }: { tool: ToolCall; result?: { content: string; is_error: boolean } }) {
51
+ const [expanded, setExpanded] = useState(false)
52
+ const color = toolBarColor(tool.name)
53
+ const mcp = parseMcpTool(tool.name)
54
+ const Icon = getToolIcon(tool.name)
55
+ const arg = getToolArg(tool)
56
+ const displayName = mcp ? `${mcp.server} · ${mcp.tool}` : tool.name
57
+
58
+ return (
59
+ <div
60
+ className="overflow-hidden rounded-lg border text-sm font-mono"
61
+ style={{
62
+ borderColor: categoryColorMix(color, 32),
63
+ backgroundColor: categoryColorMix(color, 9),
64
+ }}
65
+ >
66
+ <Button
67
+ type="button"
68
+ variant="outline"
69
+ size="sm"
70
+ onClick={() => setExpanded(e => !e)}
71
+ className={cn(
72
+ 'h-auto min-h-8 w-full justify-between gap-2 rounded-none border-0 bg-transparent px-2.5 py-2 text-left shadow-none hover:bg-muted/50',
73
+ 'font-mono text-sm'
74
+ )}
75
+ style={{ color: 'var(--foreground)' }}
76
+ >
77
+ <span className="flex min-w-0 flex-1 items-center gap-2">
78
+ <Icon className="h-3.5 w-3.5 shrink-0 opacity-80" style={{ color }} />
79
+ <span className="font-bold" style={{ color }}>
80
+ {displayName}
81
+ </span>
82
+ {arg ? <span className="truncate text-muted-foreground">{arg}</span> : null}
83
+ {result?.is_error ? (
84
+ <span className="shrink-0 rounded border border-red-500/30 bg-red-500/10 px-1.5 py-0 text-[10px] font-semibold uppercase tracking-wide text-red-400">
85
+ Error
86
+ </span>
87
+ ) : null}
88
+ </span>
89
+ <ChevronDown
90
+ className={cn('h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200', expanded && 'rotate-180')}
91
+ />
92
+ </Button>
93
+ {expanded && (
94
+ <div className="space-y-2 border-t px-2.5 py-2.5" style={{ borderColor: categoryColorMix(color, 24) }}>
95
+ <div>
96
+ <p className="mb-1 text-[11px] font-medium uppercase tracking-wide text-muted-foreground/70">Input</p>
97
+ <pre className="max-h-32 overflow-auto whitespace-pre-wrap break-all rounded-md border border-border/50 bg-background/80 p-2 text-xs text-muted-foreground">
98
+ {truncate(JSON.stringify(tool.input, null, 2), 500)}
99
+ </pre>
100
+ </div>
101
+ {result && (
102
+ <div>
103
+ <p
104
+ className={cn(
105
+ 'mb-1 text-[11px] font-medium uppercase tracking-wide',
106
+ result.is_error ? 'text-red-400' : 'text-muted-foreground/70'
107
+ )}
108
+ >
109
+ {result.is_error ? 'Error' : 'Result'}
110
+ </p>
111
+ <pre
112
+ className={cn(
113
+ 'max-h-32 overflow-auto whitespace-pre-wrap break-all rounded-md border p-2 text-xs',
114
+ result.is_error
115
+ ? 'border-red-500/25 bg-red-950/20 text-red-200/90'
116
+ : 'border-border/50 bg-background/80 text-muted-foreground'
117
+ )}
118
+ >
119
+ {truncate(result.content, 500)}
120
+ </pre>
121
+ </div>
122
+ )}
123
+ </div>
124
+ )}
125
+ </div>
126
+ )
127
+ }