diff --git a/CHANGELOG.md b/CHANGELOG.md index 64ff5fc..60122ba 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added + +- **Status bar sparkline** (`claudeStatus.statusBar.showSparkline`, default `false`) — + show a compact utilization meter (`▰▰▰▱▱`) next to the 5h percentage so remaining + capacity is visible at a glance. Custom status bar formats gain matching + `${bar5h}` / `${bar7d}` placeholders. + --- ## [0.6.2] — 2026-06-13 diff --git a/README.ja.md b/README.ja.md index 715a308..7f81d46 100644 --- a/README.ja.md +++ b/README.ja.md @@ -212,6 +212,7 @@ npm run package # → vscode-claude-status-*.vsix | `claudeStatus.statusBar.alignment` | `"left"` \| `"right"` | `"left"` | ステータスバーの位置 | | `claudeStatus.statusBar.showProjectCost` | `boolean` | `true` | ステータスバーにプロジェクトコストを表示 | | `claudeStatus.statusBar.format` | `string \| null` | `null` | カスタムラベルテンプレート。プレースホルダー: `${usage5h}`, `${usage7d}`, `${cost5h}`, `${cost7d}`, `${costDay}`。`null` = デフォルト自動生成 | +| `claudeStatus.statusBar.showSparkline` | `boolean` | `false` | ステータスバーにコンパクトな 5h 利用率メーター(▰▰▰▱▱)を表示 | | `claudeStatus.cache.ttlSeconds` | `number` (60–3600) | `300` | APIキャッシュTTL(秒) | | `claudeStatus.rateLimitApi.enabled` | `boolean` | `true` | レート制限%をAnthropicAPIから取得。`false` で新規APIコール停止。キャッシュがあれば `[Xm ago]` 付きで % を継続表示 | | `claudeStatus.realtime.enabled` | `boolean` | `false` | TTL秒ごとにAPIをポーリング(`rateLimitApi.enabled` が必要) | diff --git a/README.md b/README.md index b1fe3af..30fdc12 100644 --- a/README.md +++ b/README.md @@ -232,6 +232,7 @@ All settings are under the `claudeStatus` namespace in VS Code Settings. | `claudeStatus.statusBar.alignment` | `"left"` \| `"right"` | `"left"` | Status bar position | | `claudeStatus.statusBar.showProjectCost` | `boolean` | `true` | Show project cost in status bar | | `claudeStatus.statusBar.format` | `string \| null` | `null` | Custom label template. Placeholders: `${usage5h}`, `${usage7d}`, `${cost5h}`, `${cost7d}`, `${costDay}`. `null` = default auto-generated label | +| `claudeStatus.statusBar.showSparkline` | `boolean` | `false` | Show a compact 5h utilization meter (▰▰▰▱▱) in the status bar | | `claudeStatus.cache.ttlSeconds` | `number` (60–3600) | `300` | API cache TTL in seconds | | `claudeStatus.rateLimitApi.enabled` | `boolean` | `true` | Fetch rate-limit % from Anthropic API. When disabled, no new API calls are made; cached % is still shown with a staleness indicator (`[Xm ago]`) if available | | `claudeStatus.realtime.enabled` | `boolean` | `false` | Poll rate-limit API every TTL seconds (requires `rateLimitApi.enabled`) | diff --git a/README.zh.md b/README.zh.md index acc580b..5c60c8d 100644 --- a/README.zh.md +++ b/README.zh.md @@ -207,6 +207,7 @@ npm run package # → vscode-claude-status-*.vsix | `claudeStatus.statusBar.alignment` | `"left"` \| `"right"` | `"left"` | 状态栏位置 | | `claudeStatus.statusBar.showProjectCost` | `boolean` | `true` | 在状态栏显示项目费用 | | `claudeStatus.statusBar.format` | `string \| null` | `null` | 自定义标签模板。占位符:`${usage5h}`, `${usage7d}`, `${cost5h}`, `${cost7d}`, `${costDay}`。`null` = 默认自动生成 | +| `claudeStatus.statusBar.showSparkline` | `boolean` | `false` | 在状态栏显示紧凑的 5h 利用率仪表(▰▰▰▱▱) | | `claudeStatus.cache.ttlSeconds` | `number` (60–3600) | `300` | API 缓存 TTL(秒) | | `claudeStatus.rateLimitApi.enabled` | `boolean` | `true` | 从 Anthropic API 获取速率限制 %。禁用时停止新的 API 调用;若有缓存,仍以 `[Xm ago]` 显示 % | | `claudeStatus.realtime.enabled` | `boolean` | `false` | 每隔 TTL 秒轮询速率限制 API(需启用 `rateLimitApi.enabled`) | diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index c7cd670..e274af5 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -72,7 +72,13 @@ What the status bar and dashboard show, depending on your Claude plan and key se "claudeStatus.statusBar.format": { "type": ["string", "null"], "default": null, - "description": "Custom status bar format string. Use placeholders: ${usage5h}, ${usage7d}, ${cost5h}, ${cost7d}, ${costDay}. null = default auto-generated label." + "description": "Custom status bar format string. Use placeholders: ${usage5h}, ${usage7d}, ${bar5h}, ${bar7d}, ${cost5h}, ${cost7d}, ${costDay}. null = default auto-generated label." + }, + + "claudeStatus.statusBar.showSparkline": { + "type": "boolean", + "default": false, + "description": "Show a compact utilization meter (▰▰▰▱▱) next to the 5h percentage in the status bar (percent mode, Claude.ai only). Custom formats can use the ${bar5h} / ${bar7d} placeholders instead." }, "claudeStatus.cache.ttlSeconds": { @@ -203,6 +209,8 @@ Set `claudeStatus.statusBar.format` to a template string to replace the default |-------------|------|----------------|-------| | `${usage5h}` | integer 0–100 | `35` | 5h rate-limit utilization. Always `0` for non-claude-ai providers. | | `${usage7d}` | integer 0–100 | `72` | 7d rate-limit utilization. `0` if no 7d window. | +| `${bar5h}` | 5-char meter | `▰▰▱▱▱` | 5h utilization as a compact meter (rounded to nearest fifth). | +| `${bar7d}` | 5-char meter | `▰▰▰▰▱` | 7d utilization as a compact meter. All-empty if no 7d window. | | `${cost5h}` | decimal string | `1.23` | Local token cost in the last 5h (USD, 2 decimal places). | | `${cost7d}` | decimal string | `5.67` | Local token cost in the last 7 days (USD, 2 decimal places). | | `${costDay}` | decimal string | `2.34` | Local token cost today (USD, 2 decimal places). | diff --git a/package.json b/package.json index 8a4de64..54849b6 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,11 @@ "default": null, "description": "%config.statusBar.format.desc%" }, + "claudeStatus.statusBar.showSparkline": { + "type": "boolean", + "default": false, + "description": "%config.statusBar.showSparkline.desc%" + }, "claudeStatus.cache.ttlSeconds": { "type": "number", "default": 300, diff --git a/package.nls.ja.json b/package.nls.ja.json index 56f4a41..ccf46b5 100644 --- a/package.nls.ja.json +++ b/package.nls.ja.json @@ -10,6 +10,7 @@ "config.statusBar.alignment.desc": "ステータスバーアイテムの位置。", "config.statusBar.showProjectCost.desc": "ステータスバーに現在のプロジェクトコストを表示する。", "config.statusBar.format.desc": "ステータスバーのカスタム書式文字列。使用可能なプレースホルダー: ${usage5h}(5h 利用率 0-100)、${usage7d}(7d 利用率 0-100)、${cost5h}(5h コスト、例: 1.23)、${cost7d}(7d コスト)、${costDay}(本日のコスト)。null でデフォルト書式を使用します。", + "config.statusBar.showSparkline.desc": "ステータスバーの 5h パーセンテージの横にコンパクトな利用率メーター(▰▰▰▱▱)を表示します(percent モード、Claude.ai のみ)。カスタム書式では代わりに ${bar5h} / ${bar7d} プレースホルダを使用できます。", "config.cache.ttlSeconds.desc": "キャッシュの有効期間(秒)。Claude Code がアイドル中はこの間隔より頻繁に API を呼び出しません。", "config.rateLimitApi.enabled.desc": "Anthropic API からレート制限の利用率(5h/7d %)を取得する。無効にすると、ローカルのトークンコストのみ表示されます(レート制限の割合は非表示)。デフォルト: true — Claude Code が実行中のときのみ API を呼び出すため消費は無視できる程度です。", "config.realtime.enabled.desc": "Claude Code のアクティビティに関係なく、cache.ttlSeconds ごとにレート制限 API をポーリングします。rateLimitApi.enabled が true である必要があります。API 使用量が増加します。", diff --git a/package.nls.json b/package.nls.json index 9c882f9..5942804 100644 --- a/package.nls.json +++ b/package.nls.json @@ -10,6 +10,7 @@ "config.statusBar.alignment.desc": "Position of the status bar item.", "config.statusBar.showProjectCost.desc": "Show current project cost in the status bar.", "config.statusBar.format.desc": "Custom status bar format string. Use placeholders: ${usage5h} (5h utilization 0-100), ${usage7d} (7d utilization 0-100), ${cost5h} (5h cost, e.g. 1.23), ${cost7d} (7d cost), ${costDay} (today's cost). Set to null to use the default format.", + "config.statusBar.showSparkline.desc": "Show a compact utilization meter (▰▰▰▱▱) next to the 5h percentage in the status bar (percent mode, Claude.ai only). Custom formats can use the ${bar5h} / ${bar7d} placeholders instead.", "config.cache.ttlSeconds.desc": "Cache TTL in seconds. API is not called more often than this when Claude Code is idle.", "config.rateLimitApi.enabled.desc": "Enable fetching rate-limit utilization (5h/7d %) from the Anthropic API. When disabled, only local token cost is shown (no rate-limit percentages). Default: true — API consumption is negligible because calls are made only when Claude Code is actively running.", "config.realtime.enabled.desc": "Poll the rate-limit API every cache.ttlSeconds regardless of Claude Code activity. Requires rateLimitApi.enabled to be true. Increases API usage.", diff --git a/package.nls.zh-cn.json b/package.nls.zh-cn.json index f77b2fc..2579dd8 100644 --- a/package.nls.zh-cn.json +++ b/package.nls.zh-cn.json @@ -10,6 +10,7 @@ "config.statusBar.alignment.desc": "状态栏项目的位置。", "config.statusBar.showProjectCost.desc": "在状态栏中显示当前项目费用。", "config.statusBar.format.desc": "状态栏自定义格式字符串。可用占位符:${usage5h}(5h 利用率 0-100)、${usage7d}(7d 利用率 0-100)、${cost5h}(5h 费用,例如 1.23)、${cost7d}(7d 费用)、${costDay}(今日费用)。设为 null 使用默认格式。", + "config.statusBar.showSparkline.desc": "在状态栏 5h 百分比旁显示紧凑的利用率仪表(▰▰▰▱▱)(percent 模式,仅 Claude.ai)。自定义格式可改用 ${bar5h} / ${bar7d} 占位符。", "config.cache.ttlSeconds.desc": "缓存有效期(秒)。Claude Code 空闲时,API 调用频率不会超过此间隔。", "config.rateLimitApi.enabled.desc": "启用从 Anthropic API 获取速率限制利用率(5h/7d %)。禁用时,仅显示本地令牌费用(不显示速率限制百分比)。默认值:true — 仅在 Claude Code 活跃运行时才调用 API,消耗可忽略不计。", "config.realtime.enabled.desc": "无论 Claude Code 活动状态如何,每隔 cache.ttlSeconds 轮询速率限制 API。需要 rateLimitApi.enabled 为 true。会增加 API 使用量。", diff --git a/src/config.ts b/src/config.ts index f3aaa9b..757de1d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -23,6 +23,10 @@ export class ExtensionConfig { return this.cfg.get('statusBar.format', null); } + get showSparkline(): boolean { + return this.cfg.get('statusBar.showSparkline', false); + } + get cacheTtlSeconds(): number { return this.cfg.get('cache.ttlSeconds', 300); } diff --git a/src/statusBar.ts b/src/statusBar.ts index 81389c4..3d94f9c 100644 --- a/src/statusBar.ts +++ b/src/statusBar.ts @@ -33,6 +33,13 @@ function buildBar(utilization: number, width: number): string { return 'X'.repeat(filled) + '.'.repeat(width - filled); } +// Compact utilization meter for the status bar label (e.g. "▰▰▰▱▱" at 60%) +export function buildUtilizationBar(utilization: number, width = 5): string { + const clamped = Math.min(1, Math.max(0, utilization)); + const filled = Math.round(clamped * width); + return '▰'.repeat(filled) + '▱'.repeat(width - filled); +} + function truncateName(name: string): string { return name.length > 12 ? name.slice(0, 11) + '…' : name; } @@ -42,6 +49,8 @@ function applyFormat(format: string, data: ClaudeUsageData): string { return format .replace(/\$\{usage5h\}/g, `${Math.round(utilization5h * 100)}`) .replace(/\$\{usage7d\}/g, `${Math.round(utilization7d * 100)}`) + .replace(/\$\{bar5h\}/g, buildUtilizationBar(utilization5h)) + .replace(/\$\{bar7d\}/g, buildUtilizationBar(utilization7d)) .replace(/\$\{cost5h\}/g, cost5h.toFixed(2)) .replace(/\$\{cost7d\}/g, cost7d.toFixed(2)) .replace(/\$\{costDay\}/g, costDay.toFixed(2)); @@ -81,8 +90,9 @@ export function buildLabel(data: ClaudeUsageData, projectCosts: ProjectCostData[ part5h = `5h:100%✗`; part7d = ''; } else { + const spark5h = config.showSparkline ? `${buildUtilizationBar(utilization5h)} ` : ''; const warn5h = utilization5h >= 0.75 ? '⚠' : ''; - part5h = `5h:${formatPercent(utilization5h)}${warn5h}`; + part5h = `5h:${spark5h}${formatPercent(utilization5h)}${warn5h}`; if (has7dLimit) { const warn7d = utilization7d >= 0.75 ? '⚠' : ''; part7d = ` 7d:${formatPercent(utilization7d)}${warn7d}`; diff --git a/src/test/suite/statusBar.test.ts b/src/test/suite/statusBar.test.ts index 4762ca1..9c13b95 100644 --- a/src/test/suite/statusBar.test.ts +++ b/src/test/suite/statusBar.test.ts @@ -1,5 +1,5 @@ import * as assert from 'assert'; -import { buildLabel, buildTooltip, formatDuration } from '../../statusBar'; +import { buildLabel, buildTooltip, buildUtilizationBar, formatDuration } from '../../statusBar'; import { ClaudeUsageData } from '../../data/dataManager'; function makeData(overrides: Partial = {}): ClaudeUsageData { @@ -54,6 +54,30 @@ suite('formatDuration', () => { }); }); +suite('buildUtilizationBar', () => { + test('renders empty, partial, and full bars', () => { + assert.strictEqual(buildUtilizationBar(0), '▱▱▱▱▱'); + assert.strictEqual(buildUtilizationBar(0.6), '▰▰▰▱▱'); + assert.strictEqual(buildUtilizationBar(1), '▰▰▰▰▰'); + }); + + test('rounds to the nearest cell', () => { + assert.strictEqual(buildUtilizationBar(0.09), '▱▱▱▱▱'); // 0.45 cells → 0 + assert.strictEqual(buildUtilizationBar(0.11), '▰▱▱▱▱'); // 0.55 cells → 1 + assert.strictEqual(buildUtilizationBar(0.5), '▰▰▰▱▱'); // 2.5 cells → 3 (round half up) + }); + + test('clamps out-of-range utilization', () => { + assert.strictEqual(buildUtilizationBar(-0.5), '▱▱▱▱▱'); + assert.strictEqual(buildUtilizationBar(1.7), '▰▰▰▰▰'); + }); + + test('respects a custom width', () => { + assert.strictEqual(buildUtilizationBar(0.5, 8), '▰▰▰▰▱▱▱▱'); + assert.strictEqual(buildUtilizationBar(0.5, 8).length, 8); + }); +}); + suite('StatusBar', () => { test('buildLabel shows not-logged-in for no-credentials', () => { const label = buildLabel(makeData({ dataSource: 'no-credentials' }));