Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Export Usage Data command** (`Claude Status: Export Usage Data...`) — export the
daily usage history (date, cost, message count, tokens; same aggregation as the
heatmap, covering `claudeStatus.heatmap.days` days) to a **CSV** or **JSON** file
via a save dialog. Useful for expense reports and external analysis.

---

## [0.6.2] — 2026-06-13
Expand Down
1 change: 1 addition & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ npm run package # → vscode-claude-status-*.vsix
| **Claude Status: Open Dashboard** | ダッシュボードパネルを開く |
| **Claude Status: Toggle % / $ Display** | 表示モードの切り替え |
| **Claude Status: Set Budget…** | 日次USD予算を設定または無効化 |
| **Claude Status: 使用履歴をエクスポート…** | 日次使用履歴を CSV / JSON にエクスポート |

---

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ The extension activates automatically on VS Code startup (`onStartupFinished`).
| **Claude Status: Open Dashboard** | Open dashboard panel |
| **Claude Status: Toggle % / $ Display** | Switch display mode |
| **Claude Status: Set Budget…** | Set or disable daily USD budget |
| **Claude Status: Export Usage Data…** | Export daily usage history to CSV or JSON |

---

Expand Down
1 change: 1 addition & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ npm run package # → vscode-claude-status-*.vsix
| **Claude Status: Open Dashboard** | 打开仪表板面板 |
| **Claude Status: Toggle % / $ Display** | 切换显示模式 |
| **Claude Status: Set Budget…** | 设置或禁用每日 USD 预算 |
| **Claude Status: 导出使用历史…** | 将每日使用历史导出为 CSV 或 JSON |

---

Expand Down
16 changes: 16 additions & 0 deletions docs/SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,26 @@ Set `claudeStatus.statusBar.format` to a template string to replace the default
{
"command": "vscode-claude-status.setBudget",
"title": "Claude Status: Set Budget..."
},
{
"command": "vscode-claude-status.exportUsage",
"title": "Claude Status: Export Usage Data..."
}
]
```

### Export Usage Data

`vscode-claude-status.exportUsage` exports the daily usage history (the same
aggregation that powers the heatmap, covering `claudeStatus.heatmap.days` days)
to a file chosen via a save dialog:

- **CSV** — `date,cost_usd,messages,tokens`, one row per day (zero-activity days
included), cost with 4 decimal places.
- **JSON** — `{ exportedAt, days, totalCostUsd, daily: [{date, costUsd, messages, tokens}] }`.

Serializers live in `src/data/exportUsage.ts` (pure functions, unit-tested).

---

## Keybindings (`package.json` contributes.keybindings)
Expand Down
8 changes: 7 additions & 1 deletion l10n/bundle.l10n.ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,11 @@
"⚙ Set format": "⚙ 書式を設定",
"Format string:": "書式文字列:",
"Reset to default": "デフォルトに戻す",
"(default)": "(デフォルト)"
"(default)": "(デフォルト)",
"Spreadsheet-friendly (date, cost, messages, tokens)": "表計算向け(日付、コスト、メッセージ数、トークン数)",
"Machine-readable with export metadata": "エクスポートメタデータ付きの機械可読形式",
"Select export format": "エクスポート形式を選択",
"No usage history to export yet.": "エクスポートできる使用履歴がまだありません。",
"Usage history exported to {0}": "使用履歴を {0} にエクスポートしました",
"Failed to export usage history: {0}": "使用履歴のエクスポートに失敗しました: {0}"
}
8 changes: 7 additions & 1 deletion l10n/bundle.l10n.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,11 @@
"⚙ Set format": "⚙ 设置格式",
"Format string:": "格式字符串:",
"Reset to default": "恢复默认",
"(default)": "(默认)"
"(default)": "(默认)",
"Spreadsheet-friendly (date, cost, messages, tokens)": "适合电子表格(日期、费用、消息数、令牌数)",
"Machine-readable with export metadata": "带导出元数据的机器可读格式",
"Select export format": "选择导出格式",
"No usage history to export yet.": "暂无可导出的使用历史。",
"Usage history exported to {0}": "使用历史已导出到 {0}",
"Failed to export usage history: {0}": "导出使用历史失败: {0}"
}
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@
{
"command": "vscode-claude-status.setBudget",
"title": "%cmd.setBudget%"
},
{
"command": "vscode-claude-status.exportUsage",
"title": "%cmd.exportUsage%"
}
],
"keybindings": [
Expand Down
1 change: 1 addition & 0 deletions package.nls.ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"cmd.refresh": "Claude Status: 今すぐ更新",
"cmd.toggleDisplayMode": "Claude Status: % / $ 表示を切替",
"cmd.setBudget": "Claude Status: 予算を設定...",
"cmd.exportUsage": "Claude Status: 使用履歴をエクスポート...",
"config.title": "Claude Status",
"config.displayMode.desc": "ステータスバーの表示モード: 利用率(%) または USD コスト。",
"config.statusBar.alignment.desc": "ステータスバーアイテムの位置。",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"cmd.refresh": "Claude Status: Refresh Now",
"cmd.toggleDisplayMode": "Claude Status: Toggle % / $ Display",
"cmd.setBudget": "Claude Status: Set Budget...",
"cmd.exportUsage": "Claude Status: Export Usage Data...",
"config.title": "Claude Status",
"config.displayMode.desc": "Status bar display mode: utilization percentage or USD cost.",
"config.statusBar.alignment.desc": "Position of the status bar item.",
Expand Down
1 change: 1 addition & 0 deletions package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"cmd.refresh": "Claude Status: 立即刷新",
"cmd.toggleDisplayMode": "Claude Status: 切换 % / $ 显示",
"cmd.setBudget": "Claude Status: 设置预算...",
"cmd.exportUsage": "Claude Status: 导出使用历史...",
"config.title": "Claude Status",
"config.displayMode.desc": "状态栏显示模式:利用率百分比或 USD 费用。",
"config.statusBar.alignment.desc": "状态栏项目的位置。",
Expand Down
31 changes: 31 additions & 0 deletions src/data/exportUsage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { DailyUsage } from '../webview/heatmap';

// Serializers for the "Export Usage Data" command. Pure functions — no vscode
// dependency — so they are unit-testable outside the extension host.

export function dailyUsageToCsv(daily: DailyUsage[]): string {
const lines = ['date,cost_usd,messages,tokens'];
for (const d of daily) {
lines.push(`${d.date},${d.cost.toFixed(4)},${d.sessionCount},${d.tokensTotal}`);
}
return lines.join('\n') + '\n';
}

export function dailyUsageToJson(daily: DailyUsage[]): string {
const totalCostUsd = daily.reduce((sum, d) => sum + d.cost, 0);
return JSON.stringify(
{
exportedAt: new Date().toISOString(),
days: daily.length,
totalCostUsd: Number(totalCostUsd.toFixed(4)),
daily: daily.map(d => ({
date: d.date,
costUsd: Number(d.cost.toFixed(4)),
messages: d.sessionCount,
tokens: d.tokensTotal,
})),
},
null,
2
) + '\n';
}
48 changes: 48 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as vscode from 'vscode';
import * as os from 'os';
import * as path from 'path';
import { DataManager, ClaudeUsageData, PredictionData } from './data/dataManager';
import { dailyUsageToCsv, dailyUsageToJson } from './data/exportUsage';
import { StatusBarManager } from './statusBar';
import { config } from './config';

Expand Down Expand Up @@ -107,6 +110,51 @@ export function activate(context: vscode.ExtensionContext) {
: vscode.l10n.t('Daily budget set to ${0}.', value.toFixed(2))
);
}),
vscode.commands.registerCommand('vscode-claude-status.exportUsage', async () => {
type FormatPick = vscode.QuickPickItem & { format: 'csv' | 'json' };
const picks: FormatPick[] = [
{
label: 'CSV',
description: vscode.l10n.t('Spreadsheet-friendly (date, cost, messages, tokens)'),
format: 'csv',
},
{
label: 'JSON',
description: vscode.l10n.t('Machine-readable with export metadata'),
format: 'json',
},
];
const pick = await vscode.window.showQuickPick(picks, {
placeHolder: vscode.l10n.t('Select export format'),
});
if (!pick) { return; } // cancelled

const heatmap = await dataManager.getHeatmapData();
const daily = heatmap?.daily ?? [];
if (daily.every(d => d.sessionCount === 0)) {
vscode.window.showWarningMessage(vscode.l10n.t('No usage history to export yet.'));
return;
}

const today = new Date().toISOString().slice(0, 10);
const uri = await vscode.window.showSaveDialog({
defaultUri: vscode.Uri.file(path.join(os.homedir(), `claude-usage-${today}.${pick.format}`)),
filters: pick.format === 'csv' ? { CSV: ['csv'] } : { JSON: ['json'] },
});
if (!uri) { return; } // cancelled

const content = pick.format === 'csv' ? dailyUsageToCsv(daily) : dailyUsageToJson(daily);
try {
await vscode.workspace.fs.writeFile(uri, Buffer.from(content, 'utf8'));
vscode.window.showInformationMessage(
vscode.l10n.t('Usage history exported to {0}', uri.fsPath)
);
} catch (err) {
vscode.window.showErrorMessage(
vscode.l10n.t('Failed to export usage history: {0}', String(err))
);
}
}),
);

// React to data updates (usage + project costs are refreshed together)
Expand Down
58 changes: 58 additions & 0 deletions src/test/suite/exportUsage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as assert from 'assert';
import { dailyUsageToCsv, dailyUsageToJson } from '../../data/exportUsage';
import type { DailyUsage } from '../../webview/heatmap';

const sample: DailyUsage[] = [
{ date: '2026-07-05', cost: 1.23456, sessionCount: 12, tokensTotal: 45000 },
{ date: '2026-07-06', cost: 0, sessionCount: 0, tokensTotal: 0 },
{ date: '2026-07-07', cost: 2.5, sessionCount: 3, tokensTotal: 9000 },
];

suite('ExportUsage', () => {
suite('dailyUsageToCsv', () => {
test('emits header plus one row per day, newline-terminated', () => {
const csv = dailyUsageToCsv(sample);
const lines = csv.split('\n');
assert.strictEqual(lines[0], 'date,cost_usd,messages,tokens');
assert.strictEqual(lines.length, 5, 'header + 3 rows + trailing newline');
assert.strictEqual(lines[4], '');
});

test('formats cost with 4 decimal places', () => {
const csv = dailyUsageToCsv(sample);
assert.ok(csv.includes('2026-07-05,1.2346,12,45000'), csv);
assert.ok(csv.includes('2026-07-06,0.0000,0,0'), csv);
});

test('handles empty input', () => {
assert.strictEqual(dailyUsageToCsv([]), 'date,cost_usd,messages,tokens\n');
});
});

suite('dailyUsageToJson', () => {
test('round-trips through JSON.parse with expected shape', () => {
const parsed = JSON.parse(dailyUsageToJson(sample));
assert.strictEqual(parsed.days, 3);
assert.strictEqual(parsed.daily.length, 3);
assert.deepStrictEqual(parsed.daily[0], {
date: '2026-07-05',
costUsd: 1.2346,
messages: 12,
tokens: 45000,
});
assert.ok(!isNaN(new Date(parsed.exportedAt).getTime()), 'exportedAt is a valid date');
});

test('totalCostUsd sums all days', () => {
const parsed = JSON.parse(dailyUsageToJson(sample));
assert.strictEqual(parsed.totalCostUsd, 3.7346);
});

test('handles empty input', () => {
const parsed = JSON.parse(dailyUsageToJson([]));
assert.strictEqual(parsed.days, 0);
assert.strictEqual(parsed.totalCostUsd, 0);
assert.deepStrictEqual(parsed.daily, []);
});
});
});
Loading