@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,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
|
+
“{event.summary}”
|
|
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
|
+
}
|