claude-code-best/src/context/stats.tsx
claude-code-best 5b1a52b8e0
更新大量 tsx 原始文件; 已经迁移 login panel; 部分 (#121)
* style(B1-1): 格式化 ink/buddy/cli/context/screens/tasks/services/keybindings/state (43 files)

纯格式化:移除分号、React Compiler import、import 多行展开。
修复了 Box.tsx 和 ScrollBox.tsx 中无效的 global.d.ts import。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-2): 格式化 commands (79 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-3): 格式化 components/messages,permissions,mcp,sandbox,shell (104 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-4): 格式化 components/PromptInput,FeedbackSurvey,tasks,agents,skills,design-system,wizard (73 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-5): 格式化 components其余 + hooks + tools (232 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(B1-6): 格式化 main/entrypoints/utils/moreright (21 files)

纯格式化:移除分号、React Compiler import、import 多行展开。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: 更新 README,新增 Run.ps1/TODO.md,删除 V6.md

- README.md: 大幅重写,更详细版本历史和配置示例
- Run.ps1: 新增 Windows 启动脚本
- TODO.md: 新增包完成清单
- V6.md: 删除(架构重构规划已不适用)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: 修复以前的问题

* fix: 修复 login 面板的问题

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 23:24:27 +08:00

174 lines
4.4 KiB
TypeScript

import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
} from 'react'
import { saveCurrentProjectConfig } from '../utils/config.js'
export type StatsStore = {
increment(name: string, value?: number): void
set(name: string, value: number): void
observe(name: string, value: number): void
add(name: string, value: string): void
getAll(): Record<string, number>
}
function percentile(sorted: number[], p: number): number {
const index = (p / 100) * (sorted.length - 1)
const lower = Math.floor(index)
const upper = Math.ceil(index)
if (lower === upper) {
return sorted[lower]!
}
return sorted[lower]! + (sorted[upper]! - sorted[lower]!) * (index - lower)
}
const RESERVOIR_SIZE = 1024
type Histogram = {
reservoir: number[]
count: number
sum: number
min: number
max: number
}
export function createStatsStore(): StatsStore {
const metrics = new Map<string, number>()
const histograms = new Map<string, Histogram>()
const sets = new Map<string, Set<string>>()
return {
increment(name: string, value = 1) {
metrics.set(name, (metrics.get(name) ?? 0) + value)
},
set(name: string, value: number) {
metrics.set(name, value)
},
observe(name: string, value: number) {
let h = histograms.get(name)
if (!h) {
h = { reservoir: [], count: 0, sum: 0, min: value, max: value }
histograms.set(name, h)
}
h.count++
h.sum += value
if (value < h.min) {
h.min = value
}
if (value > h.max) {
h.max = value
}
// Reservoir sampling (Algorithm R)
if (h.reservoir.length < RESERVOIR_SIZE) {
h.reservoir.push(value)
} else {
const j = Math.floor(Math.random() * h.count)
if (j < RESERVOIR_SIZE) {
h.reservoir[j] = value
}
}
},
add(name: string, value: string) {
let s = sets.get(name)
if (!s) {
s = new Set()
sets.set(name, s)
}
s.add(value)
},
getAll() {
const result: Record<string, number> = Object.fromEntries(metrics)
for (const [name, h] of histograms) {
if (h.count === 0) {
continue
}
result[`${name}_count`] = h.count
result[`${name}_min`] = h.min
result[`${name}_max`] = h.max
result[`${name}_avg`] = h.sum / h.count
const sorted = [...h.reservoir].sort((a, b) => a - b)
result[`${name}_p50`] = percentile(sorted, 50)
result[`${name}_p95`] = percentile(sorted, 95)
result[`${name}_p99`] = percentile(sorted, 99)
}
for (const [name, s] of sets) {
result[name] = s.size
}
return result
},
}
}
export const StatsContext = createContext<StatsStore | null>(null)
type Props = {
store?: StatsStore
children: React.ReactNode
}
export function StatsProvider({
store: externalStore,
children,
}: Props): React.ReactNode {
const internalStore = useMemo(() => createStatsStore(), [])
const store = externalStore ?? internalStore
useEffect(() => {
const flush = () => {
const metrics = store.getAll()
if (Object.keys(metrics).length > 0) {
saveCurrentProjectConfig(current => ({
...current,
lastSessionMetrics: metrics,
}))
}
}
process.on('exit', flush)
return () => {
process.off('exit', flush)
}
}, [store])
return <StatsContext.Provider value={store}>{children}</StatsContext.Provider>
}
export function useStats(): StatsStore {
const store = useContext(StatsContext)
if (!store) {
throw new Error('useStats must be used within a StatsProvider')
}
return store
}
export function useCounter(name: string): (value?: number) => void {
const store = useStats()
return useCallback(
(value?: number) => store.increment(name, value),
[store, name],
)
}
export function useGauge(name: string): (value: number) => void {
const store = useStats()
return useCallback((value: number) => store.set(name, value), [store, name])
}
export function useTimer(name: string): (value: number) => void {
const store = useStats()
return useCallback(
(value: number) => store.observe(name, value),
[store, name],
)
}
export function useSet(name: string): (value: string) => void {
const store = useStats()
return useCallback((value: string) => store.add(name, value), [store, name])
}