mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-10 01:29:17 +00:00
* docs(stats): add dashboard design spec and implementation plan
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(stats): add cross-session usage tracking service
Add usageHistoryService to core with JSONL-based persistence, session
replay from chat history with sessionId deduplication, time-range
aggregation, and per-model/tool/file breakdown including latency fields.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(stats): add stats data service and ASCII chart utilities
Add statsDataService for delta calculations, efficiency metrics, tool
leaderboard, and heatmap/trend data. Add asciiCharts with braille line
chart (Bresenham rendering) and GitHub-style contribution heatmap.
Includes 38 unit tests covering both modules.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(stats): implement interactive /stats dashboard
Add three-tab dialog: Session (live metrics), Activity (KPIs, heatmap,
braille token trend chart, project ranking), and Efficiency (cache rate,
tool success, latency cards, tool leaderboard, model comparison table).
Supports tab/shift-tab navigation, r to cycle time ranges (all/month/
week/today), left/right to pan months in the trend chart, esc to close.
Persist usage on /clear for accurate cross-session tracking. Update
statsCommand tests for new dialog behavior and clearCommand tests for
telemetry mock. Update /stats documentation in commands.md.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(i18n): add stats dashboard translations for all locales
Add translations for stats dashboard UI strings in zh, zh-TW, ca, de,
fr, ja, pt, ru. Add stats keys to en.js baseline and mustTranslateKeys.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(stats): use terminal default background for inactive heatmap cells
Intensity 0 cells (no activity) now render without backgroundColor,
inheriting the terminal's native background instead of a hardcoded
color that renders incorrectly across different terminal themes.
Also fix green gradient direction: brighter = more activity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(stats): use dot markers for inactive heatmap cells
Inactive cells render as '··' with no background color instead of
colored blocks, matching common contribution graph designs. Active
cells keep their green gradient backgrounds. Fix gradient direction
so brighter green = more activity.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(stats): restore full session stats from original StatsDisplay
Add back Session ID, Success Rate with color thresholds, User Agreement
rate, Performance breakdown (Wall Time, Agent Active, API Time %, Tool
Time %), and full token counts that were present in the original exit
screen but missing from the new Session tab.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(stats): address PR review — timezone bugs, double-count, cleanup
- Fix monthOffset overflow: setDate(1) before subtracting months to
prevent day-count overflow (e.g. Mar 31 → Feb)
- Fix UTC date-parse off-by-one: append 'T00:00:00' to date-only
strings in calculateStreaks and HeatmapView fmtDate
- Fix current session double-counted after rebuild: deduplicate by
sessionId when injecting live session into loadStatsData
- Remove unused bodyWidth prop from SessionTab
- Remove 13 unused i18n keys (Overview, Favorite model, etc.)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(stats): add NaN guard, catch unhandled promise, fix useEffect race
- Guard against malformed chat records with NaN timestamps in rebuild
- Add .catch() to loadStatsData promise to prevent TUI crash
- Add stale flag to useEffect to prevent race on rapid range cycling
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(i18n): sync locale files with en.js baseline for CI check
Add 19 missing translations to zh-TW.js, remove extra keys from
zh.js and zh-TW.js that were deleted from en.js in prior cleanup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(stats): use fake timers in getPreviousRangeBounds tests
The test compared new Date() in the assertion against new Date() inside
the function, which could differ by 1ms across a millisecond boundary.
Pin system time to prevent flaky CI failures.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(i18n): restore Session and Success keys removed in error
These keys are still referenced by t('Session') in stats-helpers.tsx
and t('Success') in StatsEfficiencyTab.tsx. Add to en.js baseline and
restore zh/zh-TW translations.
* fix(stats): address R3 review — malformed record guard, arrow fix, error state
- Skip malformed records in aggregateUsage (missing tools/files/models)
- Use Object.create(null) to prevent prototype pollution on model names
- Fix Avg Latency delta arrow direction (▼ for decrease, ▲ for increase)
- Clamp fmtSuccessBar to prevent RangeError on corrupt data
- Add error state UI when loadStatsData fails
* fix(stats): address R4 review — DST fix, token consistency, tests, cleanup
- Fix DST bug in getPreviousRangeBounds('today') using setDate
- Unify token counting: project ranking uses totalTokens (same as KPI)
- Add clearCommand tests for persistSessionUsage with/without activity
- Remove dead code (unreachable sorted.length check)
- Fix heatmap legend to use dot markers matching grid cells
- Add 'Failed to load stats' i18n key to en/zh/zh-TW
* fix(stats): include thoughtsTokens in totalTokens fallback calculation
* Revert "feat(input): move physical cursor to visual cursor for IME input (#4652)"
This reverts commit 77458ad21f.
* fix(stats): prevent Yoga layout from compressing StatsDialog content
Add flexShrink={0} to the outer Box of StatsDialog so that Yoga's
default flex-shrink behavior does not compress KPI cards, charts, and
tables when the dialog exceeds the available terminal height. The parent
container in DefaultAppLayout already applies height + overflow="hidden"
to clip overflow — this fix ensures content retains its natural size
instead of being squeezed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(stats): add debug logging to catch blocks and strengthen test assertion
- Add debugLogger to all catch blocks in usageHistoryService.ts for
observability when file I/O or parsing fails
- Strengthen test assertion from toContain('Session duration') to
toContain('Session duration: 0s') to verify the zero-duration fallback
---------
Co-authored-by: a.ran <benguanran.bgr@alibaba-inc.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
215 lines
7 KiB
TypeScript
215 lines
7 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Qwen
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import type React from 'react';
|
|
import { Box, Text } from 'ink';
|
|
import { theme } from '../semantic-colors.js';
|
|
import { fmtTokens, getSeriesColors } from './stats-helpers.js';
|
|
import { useSessionStats } from '../contexts/SessionContext.js';
|
|
import { computeSessionStats } from '../utils/computeStats.js';
|
|
import { formatDuration } from '../utils/formatters.js';
|
|
import {
|
|
getStatusColor,
|
|
TOOL_SUCCESS_RATE_HIGH,
|
|
TOOL_SUCCESS_RATE_MEDIUM,
|
|
USER_AGREEMENT_RATE_HIGH,
|
|
USER_AGREEMENT_RATE_MEDIUM,
|
|
} from '../utils/displayUtils.js';
|
|
import { t } from '../../i18n/index.js';
|
|
|
|
export const SessionTab: React.FC = () => {
|
|
const SERIES_COLORS = getSeriesColors();
|
|
const { stats } = useSessionStats();
|
|
const { metrics } = stats;
|
|
const computed = computeSessionStats(metrics);
|
|
const now = new Date();
|
|
const wallDuration = stats.sessionStartTime
|
|
? now.getTime() - stats.sessionStartTime.getTime()
|
|
: 0;
|
|
|
|
let totalInput = 0;
|
|
let totalOutput = 0;
|
|
let totalCached = 0;
|
|
for (const m of Object.values(metrics.models)) {
|
|
totalInput += m.tokens.prompt;
|
|
totalOutput += m.tokens.candidates;
|
|
totalCached += m.tokens.cached;
|
|
}
|
|
const cacheRate = totalInput > 0 ? (totalCached / totalInput) * 100 : 0;
|
|
|
|
const successColor = getStatusColor(computed.successRate, {
|
|
green: TOOL_SUCCESS_RATE_HIGH,
|
|
yellow: TOOL_SUCCESS_RATE_MEDIUM,
|
|
});
|
|
const agreementColor = getStatusColor(computed.agreementRate, {
|
|
green: USER_AGREEMENT_RATE_HIGH,
|
|
yellow: USER_AGREEMENT_RATE_MEDIUM,
|
|
});
|
|
|
|
const labelWidth = 28;
|
|
|
|
return (
|
|
<Box flexDirection="column">
|
|
{/* Session ID */}
|
|
<Box>
|
|
<Box width={labelWidth}>
|
|
<Text color={theme.text.secondary}>{t('Session ID:')}</Text>
|
|
</Box>
|
|
<Text color={theme.text.primary}>{stats.sessionId}</Text>
|
|
</Box>
|
|
|
|
{/* Interaction Summary */}
|
|
<Box flexDirection="column" marginTop={1}>
|
|
<Text bold color={theme.text.primary}>
|
|
{t('Interaction Summary')}
|
|
</Text>
|
|
<Box>
|
|
<Box width={labelWidth}>
|
|
<Text color={theme.text.secondary}>{t('Tool Calls:')}</Text>
|
|
</Box>
|
|
<Text color={theme.text.primary}>
|
|
{metrics.tools.totalCalls} ({' '}
|
|
<Text color={theme.status.success}>
|
|
✓ {metrics.tools.totalSuccess}
|
|
</Text>{' '}
|
|
<Text color={theme.status.error}>✗ {metrics.tools.totalFail}</Text>{' '}
|
|
)
|
|
</Text>
|
|
</Box>
|
|
<Box>
|
|
<Box width={labelWidth}>
|
|
<Text color={theme.text.secondary}>{t('Success Rate:')}</Text>
|
|
</Box>
|
|
<Text color={successColor}>{computed.successRate.toFixed(1)}%</Text>
|
|
</Box>
|
|
{computed.totalDecisions > 0 && (
|
|
<Box>
|
|
<Box width={labelWidth}>
|
|
<Text color={theme.text.secondary}>{t('User Agreement:')}</Text>
|
|
</Box>
|
|
<Text color={agreementColor}>
|
|
{computed.agreementRate.toFixed(1)}%{' '}
|
|
<Text color={theme.text.secondary}>
|
|
({computed.totalDecisions} {t('reviewed')})
|
|
</Text>
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
{(metrics.files.totalLinesAdded > 0 ||
|
|
metrics.files.totalLinesRemoved > 0) && (
|
|
<Box>
|
|
<Box width={labelWidth}>
|
|
<Text color={theme.text.secondary}>{t('Code Changes:')}</Text>
|
|
</Box>
|
|
<Text color={theme.status.success}>
|
|
+{metrics.files.totalLinesAdded}
|
|
</Text>
|
|
<Text color={theme.text.primary}> </Text>
|
|
<Text color={theme.status.error}>
|
|
-{metrics.files.totalLinesRemoved}
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Performance */}
|
|
<Box flexDirection="column" marginTop={1}>
|
|
<Text bold color={theme.text.primary}>
|
|
{t('Performance')}
|
|
</Text>
|
|
<Box>
|
|
<Box width={labelWidth}>
|
|
<Text color={theme.text.secondary}>{t('Wall Time:')}</Text>
|
|
</Box>
|
|
<Text color={theme.text.primary}>{formatDuration(wallDuration)}</Text>
|
|
</Box>
|
|
<Box>
|
|
<Box width={labelWidth}>
|
|
<Text color={theme.text.secondary}>{t('Agent Active:')}</Text>
|
|
</Box>
|
|
<Text color={theme.text.primary}>
|
|
{formatDuration(computed.agentActiveTime)}
|
|
</Text>
|
|
</Box>
|
|
<Box paddingLeft={2}>
|
|
<Box width={26}>
|
|
<Text color={theme.text.secondary}>» {t('API Time:')}</Text>
|
|
</Box>
|
|
<Text color={theme.text.primary}>
|
|
{formatDuration(computed.totalApiTime)}{' '}
|
|
<Text color={theme.text.secondary}>
|
|
({computed.apiTimePercent.toFixed(1)}%)
|
|
</Text>
|
|
</Text>
|
|
</Box>
|
|
<Box paddingLeft={2}>
|
|
<Box width={26}>
|
|
<Text color={theme.text.secondary}>» {t('Tool Time:')}</Text>
|
|
</Box>
|
|
<Text color={theme.text.primary}>
|
|
{formatDuration(computed.totalToolTime)}{' '}
|
|
<Text color={theme.text.secondary}>
|
|
({computed.toolTimePercent.toFixed(1)}%)
|
|
</Text>
|
|
</Text>
|
|
</Box>
|
|
</Box>
|
|
|
|
{/* Token Summary */}
|
|
<Box flexDirection="column" marginTop={1}>
|
|
<Text bold color={theme.text.primary}>
|
|
{t('Tokens')}
|
|
</Text>
|
|
<Box>
|
|
<Box width={labelWidth}>
|
|
<Text color={theme.text.secondary}>{t('Input')}:</Text>
|
|
</Box>
|
|
<Text color={theme.status.warning}>
|
|
{totalInput.toLocaleString()}
|
|
</Text>
|
|
</Box>
|
|
<Box>
|
|
<Box width={labelWidth}>
|
|
<Text color={theme.text.secondary}>{t('Output')}:</Text>
|
|
</Box>
|
|
<Text color={theme.status.warning}>
|
|
{totalOutput.toLocaleString()}
|
|
</Text>
|
|
</Box>
|
|
{totalCached > 0 && (
|
|
<Box>
|
|
<Box width={labelWidth}>
|
|
<Text color={theme.text.secondary}>{t('Cached')}:</Text>
|
|
</Box>
|
|
<Text color={theme.status.success}>
|
|
{totalCached.toLocaleString()} ({cacheRate.toFixed(1)}%)
|
|
</Text>
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
|
|
{/* Models */}
|
|
{Object.keys(metrics.models).length > 0 && (
|
|
<Box flexDirection="column" marginTop={1}>
|
|
<Text bold color={theme.text.primary}>
|
|
{t('Models')}
|
|
</Text>
|
|
{Object.entries(metrics.models).map(([name, m], i) => (
|
|
<Box key={name}>
|
|
<Text color={SERIES_COLORS[i % SERIES_COLORS.length]}>● </Text>
|
|
<Text color={theme.text.primary}>{name} </Text>
|
|
<Text color={theme.text.secondary}>
|
|
{m.api.totalRequests} {t('reqs')} · {t('in')}=
|
|
{fmtTokens(m.tokens.prompt)} · {t('out')}=
|
|
{fmtTokens(m.tokens.candidates)}
|
|
</Text>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
)}
|
|
</Box>
|
|
);
|
|
};
|