diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 0000000..20f8408
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,30 @@
+{
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Run Extension",
+ "type": "extensionHost",
+ "request": "launch",
+ "args": [
+ "--extensionDevelopmentPath=${workspaceFolder}"
+ ],
+ "outFiles": [
+ "${workspaceFolder}/out/**/*.js"
+ ],
+ "preLaunchTask": "${defaultBuildTask}"
+ },
+ {
+ "name": "Extension Tests",
+ "type": "extensionHost",
+ "request": "launch",
+ "args": [
+ "--extensionDevelopmentPath=${workspaceFolder}",
+ "--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
+ ],
+ "outFiles": [
+ "${workspaceFolder}/out/test/**/*.js"
+ ],
+ "preLaunchTask": "${defaultBuildTask}"
+ }
+ ]
+}
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 0000000..3b611b4
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,27 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "npm",
+ "script": "watch",
+ "problemMatcher": "$tsc-watch",
+ "isBackground": true,
+ "presentation": {
+ "reveal": "never"
+ },
+ "group": {
+ "kind": "build",
+ "isDefault": true
+ }
+ },
+ {
+ "type": "npm",
+ "script": "compile",
+ "problemMatcher": "$tsc",
+ "presentation": {
+ "reveal": "silent"
+ },
+ "group": "build"
+ }
+ ]
+}
diff --git a/changelog.md b/changelog.md
index 5f3cd0a..ec32395 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,19 @@
# Changelog
+## [1.0.5] - 2026-02-22
+
+### Changed
+- **Auto-detect Plan Tier**: Removed manual plan tier configuration - extension now automatically detects your account level (Lite/Pro/Max) from API response
+- **Improved API Response Parsing**: Enhanced quota window detection and display logic
+
+### Added
+- **Screenshots Section**: Added UI screenshots to README for better visual preview of the extension
+- **API Documentation**: Added comprehensive API documentation in `docs/API-DOCUMENTATION.md`
+- **Debug Configuration**: Added `.vscode/launch.json` and `.vscode/tasks.json` for easier debugging
+
+### Fixed
+- **Status Bar Display**: Fixed status bar display issues and improved tooltip rendering
+
## [1.0.4] - 2026-01-03
### Changed
diff --git a/docs/API-DOCUMENTATION.md b/docs/API-DOCUMENTATION.md
new file mode 100644
index 0000000..58790c9
--- /dev/null
+++ b/docs/API-DOCUMENTATION.md
@@ -0,0 +1,460 @@
+# Z.AI Usage Monitoring API Documentation
+
+> **Status**: ✅ Verified (2026-02-21)
+> **Base URL**: `https://api.z.ai`
+> **API Type**: Internal/Private (from zai-org/zai-coding-plugins)
+
+## Authentication
+
+All endpoints require Bearer token authentication via the `Authorization` header:
+
+```http
+Authorization: your-api-key-here
+```
+
+## Common Headers
+
+```http
+Authorization: {API_KEY}
+Content-Type: application/json
+Accept-Language: en-US,en
+```
+
+---
+
+## 1. Model Usage Query
+
+Query model call counts and token usage over a time range.
+
+### Endpoint
+
+```
+GET /api/monitor/usage/model-usage
+```
+
+### Query Parameters
+
+| Parameter | Type | Required | Format | Description |
+| ----------- | ------ | -------- | --------------------- | ------------------------- |
+| `startTime` | string | Yes | `yyyy-MM-dd HH:mm:ss` | Start time of query range |
+| `endTime` | string | Yes | `yyyy-MM-dd HH:mm:ss` | End time of query range |
+
+### Example Request
+
+```http
+GET /api/monitor/usage/model-usage?startTime=2026-02-20%2020%3A00%3A00&endTime=2026-02-21%2020%3A59%3A59
+Authorization: your-api-key-here
+Accept-Language: en-US,en
+```
+
+### Example Response
+
+```json
+{
+ "code": 200,
+ "msg": "Operation successful",
+ "data": {
+ "x_time": [
+ "2026-02-20 20:00",
+ "2026-02-20 21:00",
+ "2026-02-20 22:00"
+ ],
+ "modelCallCount": [
+ 137,
+ 36,
+ 79
+ ],
+ "tokensUsage": [
+ 4689148,
+ 1343019,
+ 3506363
+ ],
+ "totalUsage": {
+ "totalModelCallCount": 1227,
+ "totalTokensUsage": 45867924
+ }
+ },
+ "success": true
+}
+```
+
+### Response Fields
+
+| Field | Type | Description |
+| ------------------------------------- | ---------------- | -------------------------------------- |
+| `code` | number | HTTP status code |
+| `msg` | string | Response message |
+| `success` | boolean | Whether the request succeeded |
+| `data.x_time` | string[] | Array of hourly timestamps |
+| `data.modelCallCount` | (number\|null)[] | Model calls per hour (null if no data) |
+| `data.tokensUsage` | (number\|null)[] | Tokens used per hour (null if no data) |
+| `data.totalUsage.totalModelCallCount` | number | Total model calls in time range |
+| `data.totalUsage.totalTokensUsage` | number | Total tokens used in time range |
+
+---
+
+## 2. Tool Usage Query
+
+Query MCP tool usage (network search, web reader, zread) over a time range.
+
+### Endpoint
+
+```
+GET /api/monitor/usage/tool-usage
+```
+
+### Query Parameters
+
+| Parameter | Type | Required | Format | Description |
+| ----------- | ------ | -------- | --------------------- | ------------------------- |
+| `startTime` | string | Yes | `yyyy-MM-dd HH:mm:ss` | Start time of query range |
+| `endTime` | string | Yes | `yyyy-MM-dd HH:mm:ss` | End time of query range |
+
+### Example Request
+
+```http
+GET /api/monitor/usage/tool-usage?startTime=2026-02-20%2020%3A00%3A00&endTime=2026-02-21%2020%3A59%3A59
+Authorization: your-api-key-here
+Accept-Language: en-US,en
+```
+
+### Example Response
+
+```json
+{
+ "code": 200,
+ "msg": "Operation successful",
+ "data": {
+ "x_time": [
+ "2026-02-20 20:00",
+ "2026-02-20 21:00",
+ "2026-02-20 22:00"
+ ],
+ "networkSearchCount": [null, null, null],
+ "webReadMcpCount": [null, null, null],
+ "zreadMcpCount": [null, null, null],
+ "totalUsage": {
+ "totalNetworkSearchCount": 0,
+ "totalWebReadMcpCount": 0,
+ "totalZreadMcpCount": 0,
+ "totalSearchMcpCount": 0,
+ "toolDetails": []
+ }
+ },
+ "success": true
+}
+```
+
+### Response Fields
+
+| Field | Type | Description |
+| ----------------------------------------- | ---------------- | ----------------------------- |
+| `code` | number | HTTP status code |
+| `msg` | string | Response message |
+| `success` | boolean | Whether the request succeeded |
+| `data.x_time` | string[] | Array of hourly timestamps |
+| `data.networkSearchCount` | (number\|null)[] | Network search calls per hour |
+| `data.webReadMcpCount` | (number\|null)[] | Web reader MCP calls per hour |
+| `data.zreadMcpCount` | (number\|null)[] | Zread MCP calls per hour |
+| `data.totalUsage.totalNetworkSearchCount` | number | Total network search calls |
+| `data.totalUsage.totalWebReadMcpCount` | number | Total web reader calls |
+| `data.totalUsage.totalZreadMcpCount` | number | Total zread calls |
+| `data.totalUsage.totalSearchMcpCount` | number | Total search MCP calls |
+| `data.totalUsage.toolDetails` | array | Detailed tool usage breakdown |
+
+---
+
+## 3. Quota Limit Query
+
+Query account quota limits and current usage percentages. This endpoint does not require time parameters.
+
+### Endpoint
+
+```
+GET /api/monitor/usage/quota/limit
+```
+
+### Query Parameters
+
+None required.
+
+### Example Request
+
+```http
+GET /api/monitor/usage/quota/limit
+Authorization: your-api-key-here
+Accept-Language: en-US,en
+```
+
+### Example Response
+
+```json
+{
+ "code": 200,
+ "msg": "Operation successful",
+ "data": {
+ "limits": [
+ {
+ "type": "TOKENS_LIMIT",
+ "unit": 3,
+ "number": 5,
+ "percentage": 0
+ },
+ {
+ "type": "TOKENS_LIMIT",
+ "unit": 6,
+ "number": 1,
+ "percentage": 21,
+ "nextResetTime": 1772192697998
+ },
+ {
+ "type": "TIME_LIMIT",
+ "unit": 5,
+ "number": 1,
+ "usage": 1000,
+ "currentValue": 0,
+ "remaining": 1000,
+ "percentage": 0,
+ "nextResetTime": 1774007097985,
+ "usageDetails": [
+ {
+ "modelCode": "search-prime",
+ "usage": 0
+ },
+ {
+ "modelCode": "web-reader",
+ "usage": 0
+ },
+ {
+ "modelCode": "zread",
+ "usage": 0
+ }
+ ]
+ }
+ ],
+ "level": "pro"
+ },
+ "success": true
+}
+```
+
+### Response Fields
+
+| Field | Type | Description |
+| ------------- | ------- | ----------------------------- |
+| `code` | number | HTTP status code |
+| `msg` | string | Response message |
+| `success` | boolean | Whether the request succeeded |
+| `data.limits` | array | Array of quota limit objects |
+| `data.level` | string | Account level (e.g., "pro") |
+
+### Limit Object Fields
+
+| Field | Type | Description |
+| --------------- | ------ | ---------------------------------------------------------------------- |
+| `type` | string | Limit type: `TOKENS_LIMIT` or `TIME_LIMIT` |
+| `unit` | number | Time unit code: `3` = hour(s), `5` = month(s), `6` = week(s) |
+| `number` | number | Quantity of the time unit (e.g., unit=3, number=5 means 5-hour window) |
+| `percentage` | number | Usage percentage (0-100) |
+| `nextResetTime` | number | Unix timestamp (ms) when quota resets |
+| `usage` | number | Total quota allowed (TIME_LIMIT only) |
+| `currentValue` | number | Current usage count (TIME_LIMIT only) |
+| `remaining` | number | Remaining quota (TIME_LIMIT only) |
+| `usageDetails` | array | Per-model usage breakdown (TIME_LIMIT only) |
+
+### Unit Type Mapping
+
+| Unit Code | Time Unit | Example |
+| --------- | --------- | ------------------------------------------ |
+| `3` | Hour(s) | `unit=3, number=5` → 5-hour rolling window |
+| `5` | Month(s) | `unit=5, number=1` → 1-month quota |
+| `6` | Week(s) | `unit=6, number=1` → 1-week quota |
+
+### Limit Type Explanation
+
+- **TOKENS_LIMIT**: Token usage quota with rolling time windows. The `unit` field defines the time unit (hour/week/month), and `number` specifies the quantity (e.g., unit=3/number=5 means a 5-hour rolling window, unit=6/number=1 means a 1-week window).
+- **TIME_LIMIT**: MCP tool usage quota with fixed reset periods (e.g., monthly limit with 1000 calls).
+
+---
+
+## Error Handling
+
+All endpoints return consistent error responses:
+
+```json
+{
+ "code": 400,
+ "msg": "Error description",
+ "success": false
+}
+```
+
+### Common HTTP Status Codes
+
+| Code | Description |
+| ---- | -------------------------------- |
+| 200 | Success |
+| 400 | Bad Request (invalid parameters) |
+| 401 | Unauthorized (invalid API key) |
+| 500 | Internal Server Error |
+
+---
+
+## Usage Notes
+
+### Time Range Recommendations
+
+1. **Model Usage & Tool Usage**:
+ - Recommended range: 24-48 hours
+ - Data is returned in hourly buckets
+ - Future hours may return `null` values
+
+2. **Quota Limit**:
+ - No time parameters needed
+ - Returns current quota status and reset times
+
+### Rate Limiting
+
+No official rate limits documented, but recommended approach:
+- Cache quota limit data (updates infrequently)
+- Poll model/tool usage every 5-30 minutes
+- Avoid excessive queries (< 1 request/second)
+
+### Date Formatting
+
+Always use URL-encoded format for time parameters:
+```
+startTime=2026-02-20%2020%3A00%3A00
+```
+
+JavaScript example:
+```javascript
+const startTime = '2026-02-20 20:00:00';
+const encoded = encodeURIComponent(startTime);
+```
+
+---
+
+## Integration Example
+
+### Node.js with HTTPS
+
+```javascript
+import https from 'https';
+
+const API_KEY = 'your-api-key-here';
+const BASE_URL = 'https://api.z.ai';
+
+function queryQuotaLimit() {
+ return new Promise((resolve, reject) => {
+ const url = new URL(`${BASE_URL}/api/monitor/usage/quota/limit`);
+
+ const options = {
+ hostname: url.hostname,
+ port: 443,
+ path: url.pathname,
+ method: 'GET',
+ headers: {
+ 'Authorization': API_KEY,
+ 'Accept-Language': 'en-US,en',
+ 'Content-Type': 'application/json'
+ }
+ };
+
+ const req = https.request(options, (res) => {
+ let data = '';
+ res.on('data', (chunk) => { data += chunk; });
+ res.on('end', () => {
+ if (res.statusCode === 200) {
+ resolve(JSON.parse(data));
+ } else {
+ reject(new Error(`HTTP ${res.statusCode}: ${data}`));
+ }
+ });
+ });
+
+ req.on('error', reject);
+ req.end();
+ });
+}
+```
+
+### TypeScript Interfaces
+
+```typescript
+// Model Usage Response
+interface ModelUsageResponse {
+ code: number;
+ msg: string;
+ success: boolean;
+ data: {
+ x_time: string[];
+ modelCallCount: (number | null)[];
+ tokensUsage: (number | null)[];
+ totalUsage: {
+ totalModelCallCount: number;
+ totalTokensUsage: number;
+ };
+ };
+}
+
+// Tool Usage Response
+interface ToolUsageResponse {
+ code: number;
+ msg: string;
+ success: boolean;
+ data: {
+ x_time: string[];
+ networkSearchCount: (number | null)[];
+ webReadMcpCount: (number | null)[];
+ zreadMcpCount: (number | null)[];
+ totalUsage: {
+ totalNetworkSearchCount: number;
+ totalWebReadMcpCount: number;
+ totalZreadMcpCount: number;
+ totalSearchMcpCount: number;
+ toolDetails: any[];
+ };
+ };
+}
+
+// Quota Limit Response
+interface QuotaLimitResponse {
+ code: number;
+ msg: string;
+ success: boolean;
+ data: {
+ limits: Array<{
+ type: 'TOKENS_LIMIT' | 'TIME_LIMIT';
+ unit: number;
+ number: number;
+ percentage: number;
+ nextResetTime?: number;
+ usage?: number;
+ currentValue?: number;
+ remaining?: number;
+ usageDetails?: Array<{
+ modelCode: string;
+ usage: number;
+ }>;
+ }>;
+ level: string;
+ };
+}
+```
+
+---
+
+## Version History
+
+- **2026-02-21**: Initial documentation based on `zai-org/zai-coding-plugins` v1.x
+- Verified with API Key: `62e19ef7...` (redacted)
+- All 3 endpoints tested and confirmed working
+
+---
+
+## References
+
+- Official Plugin: https://github.com/zai-org/zai-coding-plugins
+- Plugin: `glm-plan-usage` (usage query functionality)
+- Source: `plugins/glm-plan-usage/skills/usage-query-skill/scripts/query-usage.mjs`
diff --git a/package-lock.json b/package-lock.json
index 477954e..faaca76 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "zai-usage-tracker",
- "version": "0.0.5",
+ "version": "1.0.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "zai-usage-tracker",
- "version": "0.0.5",
+ "version": "1.0.4",
"license": "MIT",
"devDependencies": {
"@types/node": "^18.0.0",
diff --git a/package.json b/package.json
index 3330af6..41e4a00 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "zai-usage-tracker",
"displayName": "Z.ai GLM Usage Tracker",
"description": "Track your Z.ai GLM Coding Plan usage in the status bar",
- "version": "1.0.4",
+ "version": "1.0.5",
"publisher": "melon-hub",
"icon": "icon.png",
"license": "MIT",
@@ -56,28 +56,12 @@
"configuration": {
"title": "Z.ai Usage Tracker",
"properties": {
- "zaiUsage.planTier": {
- "type": "string",
- "enum": [
- "lite",
- "pro",
- "max"
- ],
- "default": "lite",
- "description": "Your GLM Coding Plan tier",
- "enumDescriptions": [
- "Lite Plan: ~120 prompts every 5 hours",
- "Pro Plan: ~600 prompts every 5 hours",
- "Max Plan: ~2400 prompts every 5 hours"
- ],
- "order": 1
- },
"zaiUsage.refreshInterval": {
"type": "number",
"default": 5,
"minimum": 1,
"description": "Refresh interval in minutes",
- "order": 2
+ "order": 1
}
}
}
@@ -99,4 +83,4 @@
"eslint": "^8.45.0",
"typescript": "^5.1.0"
}
-}
+}
\ No newline at end of file
diff --git a/readme.md b/readme.md
index e9bfa25..1c8cfd5 100644
--- a/readme.md
+++ b/readme.md
@@ -6,25 +6,42 @@
A VS Code extension that tracks your Z.ai GLM Coding Plan usage and displays it in the status bar. Also works with Windsurf, VSCodium, and other VS Code forks.
+## Screenshots
+
+
+
## Features
-- **Real-time Usage Display**: See your 5-hour token quota directly in the status bar
- - Shows percentage used and current tokens
- - Example: `✓ ⚡ 1% • 14.6K tokens`
-
-- **Detailed Tooltip**: Hover to see comprehensive usage stats:
- - 5-hour token quota with progress bar
- - 7-day usage (prompts + tokens)
- - 30-day usage (prompts + tokens)
- - Connection status and last update time
-
-- **Quick Pick Menu**: Click status bar for detailed stats and actions
- - View all usage metrics
- - Refresh usage data
- - Configure settings
-
+- **Dynamic Quota Windows**: Automatically fetches all token quota windows from API
+ - 5-hour rolling window
+ - 1-week quota
+ - 1-month quota
+ - Displays the most relevant quota in status bar (prioritizes longest time window)
+
+- **Rich Tooltip Display**: Hover to see comprehensive usage stats with Markdown formatting:
+ - All token quota windows with progress bars and reset times
+ - MCP tool usage quotas (network search, web reader, zread)
+ - Today / 7-day / 30-day usage statistics
+ - Account plan level (auto-detected from API)
+ - Estimated token limits based on usage percentage
+
+- **Smart Status Bar**: Modern display with VS Code Codicon icons
+ - Example: `⚡ GLM: 21% · 45.8M / 220M Tokens`
+ - Warning background when usage ≥ 80%
+
+- **Auto-detected Plan Tier**: No manual configuration needed
+ - API automatically returns your account level (free/pro/enterprise)
+ - Displays plan tier in tooltip
+
+- **Precise Reset Times**: Shows exact time until quota resets
+ - Short periods: "in 2h 30m"
+ - Long periods: full date/time "2026-02-28 14:30"
+
+- **Debug Mode**: View raw API responses for troubleshooting
+ - Command: `Z.ai Usage Tracker: Debug: Show Raw API Responses`
+
- **Automatic Refresh**: Configurable refresh interval (default: 5 minutes)
-
+
- **Secure API Key Storage**: Uses VS Code's encrypted SecretStorage
## Installation
@@ -64,20 +81,6 @@ You need a Z.ai API key to use this extension:
4. Select "Update API Key" and paste your key
5. Your key is stored securely in VS Code's encrypted storage
-### Plan Tier
-
-Set your GLM Coding Plan tier:
-
-```json
-{
- "zaiUsage.planTier": "lite" // Options: "lite", "pro", "max"
-}
-```
-
-- **Lite**: ~120 prompts every 5 hours
-- **Pro**: ~600 prompts every 5 hours
-- **Max**: ~2400 prompts every 5 hours
-
### Refresh Interval
Set how often to fetch usage data (in minutes):
@@ -90,36 +93,43 @@ Set how often to fetch usage data (in minutes):
Minimum: 1 minute, Default: 5 minutes
+### Automatic Plan Detection
+
+Your GLM Coding Plan tier is automatically detected from the API - no manual configuration needed. The extension displays your account level (Lite/Pro/Max) in the tooltip.
+
## Usage
Once configured, the extension will:
1. Automatically activate when VS Code/Cursor starts
-2. Display usage in the status bar: `✓ ⚡ 1% • 14.6K tokens`
+2. Display usage in the status bar: `⚡ GLM: 21% · 45.8M / 220M Tokens`
3. Update periodically based on your refresh interval
-4. Show detailed tooltip on hover
-5. Provide quick actions on click
+4. Show detailed tooltip with Markdown formatting on hover
+5. Provide quick actions on click (refresh, configure settings)
## Commands
- `zaiUsage.refresh`: Manually refresh usage data
-- `zaiUsage.configure`: Open configuration menu
-- `zaiUsage.showMenu`: Show quick actions menu
+- `zaiUsage.configure`: Open configuration menu (API key and refresh interval)
+- `zaiUsage.debug`: Debug - Show raw API responses in output channel
## Status Bar Display
The status bar shows:
-- **Connection Status**: ✓ (connected) or ⚠ (offline/error)
-- **Icon**: Lightning bolt ⚡
-- **Percentage**: 5-hour token quota percentage (e.g., 1%)
-- **Tokens**: Current tokens used (e.g., 14.6K tokens)
+- **Icon**: Lightning bolt ⚡ (Codicon)
+- **Label**: "GLM:"
+- **Connection Status**: Empty (connected) or warning icon (error)
+- **Percentage**: Usage percentage of the longest time window quota (e.g., 21%)
+- **Tokens**: Actual tokens used / estimated limit (e.g., 45.8M / 220M Tokens)
-Example: `✓ ⚡ 1% • 14.6K tokens`
+Example: `⚡ GLM: 21% · 45.8M / 220M Tokens`
Background color indicates usage level:
- Normal background: < 80% quota used
- Warning background: ≥ 80% quota used
+The extension automatically selects the longest time window quota (Month > Week > Hour) for display in the status bar.
+
## Development
```bash
@@ -132,23 +142,27 @@ See [CLAUDE.md](./CLAUDE.md) for full development and publishing workflow.
## How It Works
-1. **API Service**: Attempts to fetch usage data from Z.ai's API endpoints
-2. **Fallback**: If no API endpoint is available, uses local tracking
-3. **Configuration**: Stores API key and settings in VS Code configuration
-4. **Display**: Updates status bar with current usage and progress
+1. **API Service**: Fetches usage data from Z.ai's official monitor API endpoints
+2. **Dynamic Quotas**: Retrieves all token quota windows (5-hour, 1-week, 1-month) and MCP tool limits
+3. **Auto-detection**: Plan tier is automatically detected from API response
+4. **Display**: Updates status bar with current usage, progress bars, and reset times
5. **Refresh**: Periodically fetches updated data (configurable interval)
## API Endpoints
The extension uses the official Z.ai monitor API endpoints:
-- `https://api.z.ai/api/monitor/usage/quota/limit` - 5-hour token quota
-- `https://api.z.ai/api/monitor/usage/model-usage` - Model usage stats (with time range)
+- `https://api.z.ai/api/monitor/usage/quota/limit` - Quota limits and usage percentages (returns multiple time windows)
+- `https://api.z.ai/api/monitor/usage/model-usage` - Model usage stats (prompts + tokens)
+- `https://api.z.ai/api/monitor/usage/tool-usage` - MCP tool usage stats
+
+See [API Documentation](./docs/API-DOCUMENTATION.md) for detailed API reference.
### Debugging
Run the debug command to see raw API responses:
- Command: `Z.ai Usage Tracker: Debug: Show Raw API Responses`
+- Output appears in "Z.ai API Debug" output channel
## Privacy
@@ -168,8 +182,14 @@ Run the debug command to see raw API responses:
- Check your internet connection
- Verify your API key is valid at [z.ai/manage-apikey](https://z.ai/manage-apikey/apikey-list)
-- Try clicking "Retry" in the error message
-- Run the debug command to see raw API responses
+- Try clicking "Refresh Usage"
+- Run the debug command: `Z.ai Usage Tracker: Debug: Show Raw API Responses`
+
+### Quota shows 0% or no data
+
+- This is normal for new accounts or after quota reset
+- The 5-hour quota resets every 5 hours
+- Wait a few minutes and refresh again
## License
diff --git a/screenshot.jpg b/screenshot.jpg
new file mode 100644
index 0000000..f4698f0
Binary files /dev/null and b/screenshot.jpg differ
diff --git a/src/api/zaiService.ts b/src/api/zaiService.ts
index c51162b..c621511 100644
--- a/src/api/zaiService.ts
+++ b/src/api/zaiService.ts
@@ -1,9 +1,41 @@
+/**
+ * Represents a single token quota window from TOKENS_LIMIT
+ */
+export interface TokenQuota {
+ windowName: string; // e.g., "5-Hour", "1-Week", "1-Month"
+ unit: number; // 3=hour(s), 5=month(s), 6=week(s)
+ number: number; // Quantity of the time unit
+ percentage: number; // Usage percentage (0-100)
+ nextResetTime?: number; // Unix timestamp (ms) when quota resets
+ actualTokens?: number; // Actual tokens used in this window period
+}
+
+/**
+ * Represents MCP tool usage limits from TIME_LIMIT
+ */
+export interface TimeLimit {
+ windowName: string; // e.g., "1-Month MCP Tools"
+ unit: number; // 5=month(s)
+ number: number; // Quantity of the time unit
+ percentage: number; // Usage percentage (0-100)
+ usage: number; // Total quota allowed
+ currentValue: number; // Current usage count
+ remaining: number; // Remaining quota
+ nextResetTime?: number; // Unix timestamp (ms) when quota resets
+ usageDetails?: Array<{ // Per-tool usage breakdown
+ modelCode: string;
+ usage: number;
+ }>;
+}
+
export interface UsageData {
- // 5-hour rolling window quota (token-based)
- current5HourTokens: number; // Tokens used in 5-hour window
- limit5HourTokens: number; // Token limit (e.g., 800M)
- percentage5Hour: number; // Percentage used
- quotaResetTime?: Date;
+ // Dynamic token quota windows from API
+ tokenQuotas: TokenQuota[]; // All TOKENS_LIMIT items returned by API
+ // MCP tool limits from API
+ timeLimits: TimeLimit[]; // All TIME_LIMIT items returned by API
+ // Today stats
+ todayPrompts: number;
+ todayTokens: number;
// 7-day stats
sevenDayPrompts: number;
sevenDayTokens: number;
@@ -13,6 +45,8 @@ export interface UsageData {
// Metadata
lastUpdated: Date;
connectionStatus: 'connected' | 'disconnected' | 'error';
+ // Plan level from quota limit API
+ planLevel?: string; // e.g., "free", "pro", "enterprise"
}
export interface FetchResult {
@@ -23,13 +57,18 @@ export interface FetchResult {
interface QuotaLimitResponse {
limits?: Array<{
- type: string;
+ type: 'TOKENS_LIMIT' | 'TIME_LIMIT';
+ unit: number; // 3=hour(s), 5=month(s), 6=week(s)
+ number: number; // Quantity of the time unit
percentage: number;
- currentValue?: number;
+ nextResetTime?: number;
+ // TIME_LIMIT specific fields
usage?: number;
- limit?: number;
- usageDetails?: any;
+ currentValue?: number;
+ remaining?: number;
+ usageDetails?: any[];
}>;
+ level?: string;
}
interface ModelUsageResponse {
@@ -42,12 +81,10 @@ interface ModelUsageResponse {
export class ZaiService {
private apiKey: string;
- private planLimit: number;
private baseUrl = 'https://api.z.ai';
- constructor(apiKey: string, planLimit: number) {
+ constructor(apiKey: string) {
this.apiKey = apiKey;
- this.planLimit = planLimit;
}
/**
@@ -64,58 +101,122 @@ export class ZaiService {
try {
const now = new Date();
- const formatDateTime = (date: Date): string => {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- const seconds = String(date.getSeconds()).padStart(2, '0');
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
- };
-
// Time windows for different stats
const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
+ const startToday = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0, 0);
const start7d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, 0, 0, 0, 0);
const start30d = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate(), 0, 0, 0, 0);
- const queryParams7d = `?startTime=${encodeURIComponent(formatDateTime(start7d))}&endTime=${encodeURIComponent(formatDateTime(end))}`;
- const queryParams30d = `?startTime=${encodeURIComponent(formatDateTime(start30d))}&endTime=${encodeURIComponent(formatDateTime(end))}`;
+ const queryParamsToday = `?startTime=${encodeURIComponent(this.formatDateTime(startToday))}&endTime=${encodeURIComponent(this.formatDateTime(end))}`;
+ const queryParams7d = `?startTime=${encodeURIComponent(this.formatDateTime(start7d))}&endTime=${encodeURIComponent(this.formatDateTime(end))}`;
+ const queryParams30d = `?startTime=${encodeURIComponent(this.formatDateTime(start30d))}&endTime=${encodeURIComponent(this.formatDateTime(end))}`;
// Fetch all endpoints in parallel
- const [quotaLimitResult, modelUsage7dResult, modelUsage30dResult] = await Promise.allSettled([
+ const [quotaLimitResult, modelUsageTodayResult, modelUsage7dResult, modelUsage30dResult] = await Promise.allSettled([
this.fetchEndpoint(`${this.baseUrl}/api/monitor/usage/quota/limit`, 'Quota limit'),
+ this.fetchEndpoint(`${this.baseUrl}/api/monitor/usage/model-usage${queryParamsToday}`, 'Today usage'),
this.fetchEndpoint(`${this.baseUrl}/api/monitor/usage/model-usage${queryParams7d}`, '7-day usage'),
this.fetchEndpoint(`${this.baseUrl}/api/monitor/usage/model-usage${queryParams30d}`, '30-day usage')
]);
// Initialize values
- let current5HourTokens = 0;
- let limit5HourTokens = 800000000; // 800M default
- let percentage5Hour = 0;
+ const tokenQuotas: TokenQuota[] = [];
+ const timeLimits: TimeLimit[] = [];
+ let todayPrompts = 0;
+ let todayTokens = 0;
let sevenDayPrompts = 0;
let sevenDayTokens = 0;
let thirtyDayPrompts = 0;
let thirtyDayTokens = 0;
- // Process quota limit response (5-hour token quota)
- if (quotaLimitResult.status === 'fulfilled' && quotaLimitResult.value) {
- const quotaData = quotaLimitResult.value as QuotaLimitResponse;
- if (quotaData.limits) {
- for (const limit of quotaData.limits) {
+ // Process quota limit response - collect all TOKENS_LIMIT and TIME_LIMIT windows dynamically
+ const quotaLimitResponse = quotaLimitResult.status === 'fulfilled' ? quotaLimitResult.value as QuotaLimitResponse : null;
+ let planLevel: string | undefined;
+
+ if (quotaLimitResponse) {
+ planLevel = quotaLimitResponse.level;
+ if (quotaLimitResponse.limits) {
+ for (const limit of quotaLimitResponse.limits) {
if (limit.type === 'TOKENS_LIMIT') {
- percentage5Hour = limit.percentage || 0;
- if (limit.currentValue !== undefined) {
- current5HourTokens = limit.currentValue;
- }
- if (limit.usage !== undefined) {
- limit5HourTokens = limit.usage;
- }
+ tokenQuotas.push({
+ windowName: this.formatWindowName(limit.unit, limit.number),
+ unit: limit.unit,
+ number: limit.number,
+ percentage: limit.percentage || 0,
+ nextResetTime: limit.nextResetTime,
+ actualTokens: undefined // Will be filled below
+ });
+ } else if (limit.type === 'TIME_LIMIT') {
+ timeLimits.push({
+ windowName: this.formatWindowName(limit.unit, limit.number) + ' MCP Tools',
+ unit: limit.unit,
+ number: limit.number,
+ percentage: limit.percentage || 0,
+ usage: limit.usage || 0,
+ currentValue: limit.currentValue || 0,
+ remaining: limit.remaining || 0,
+ nextResetTime: limit.nextResetTime,
+ usageDetails: limit.usageDetails
+ });
}
}
}
}
+ // Fetch actual usage for each token quota window based on its reset time
+ const quotaUsageRequests = tokenQuotas.map(async (quota) => {
+ if (!quota.nextResetTime || quota.percentage === 0) {
+ return null;
+ }
+
+ try {
+ // Calculate the start time of this quota window
+ const resetDate = new Date(quota.nextResetTime);
+ let startDate: Date;
+
+ // Calculate window start based on unit and number
+ if (quota.unit === 3) {
+ // Hours
+ startDate = new Date(resetDate.getTime() - quota.number * 3600000);
+ } else if (quota.unit === 6) {
+ // Weeks
+ startDate = new Date(resetDate.getTime() - quota.number * 7 * 86400000);
+ } else if (quota.unit === 5) {
+ // Months (approximate as 30 days)
+ startDate = new Date(resetDate.getTime() - quota.number * 30 * 86400000);
+ } else {
+ return null;
+ }
+
+ const queryParams = `?startTime=${encodeURIComponent(this.formatDateTime(startDate))}&endTime=${encodeURIComponent(this.formatDateTime(now))}`;
+ const result = await this.fetchEndpoint(
+ `${this.baseUrl}/api/monitor/usage/model-usage${queryParams}`,
+ `${quota.windowName} usage`
+ );
+
+ if (result && (result as ModelUsageResponse).totalUsage) {
+ const modelData = result as ModelUsageResponse;
+ quota.actualTokens = modelData.totalUsage?.totalTokensUsage || 0;
+ }
+ } catch (error) {
+ console.error(`Failed to fetch usage for ${quota.windowName}:`, error);
+ }
+
+ return null;
+ });
+
+ // Wait for all quota usage requests to complete
+ await Promise.allSettled(quotaUsageRequests);
+
+ // Process today model usage
+ if (modelUsageTodayResult.status === 'fulfilled' && modelUsageTodayResult.value) {
+ const modelData = modelUsageTodayResult.value as ModelUsageResponse;
+ if (modelData.totalUsage) {
+ todayPrompts = modelData.totalUsage.totalModelCallCount || 0;
+ todayTokens = modelData.totalUsage.totalTokensUsage || 0;
+ }
+ }
+
// Process 7-day model usage
if (modelUsage7dResult.status === 'fulfilled' && modelUsage7dResult.value) {
const modelData = modelUsage7dResult.value as ModelUsageResponse;
@@ -137,15 +238,17 @@ export class ZaiService {
return {
success: true,
data: {
- current5HourTokens,
- limit5HourTokens,
- percentage5Hour,
+ tokenQuotas,
+ timeLimits,
+ todayPrompts,
+ todayTokens,
sevenDayPrompts,
sevenDayTokens,
thirtyDayPrompts,
thirtyDayTokens,
lastUpdated: new Date(),
- connectionStatus: 'connected'
+ connectionStatus: 'connected',
+ planLevel
}
};
} catch (error) {
@@ -158,6 +261,38 @@ export class ZaiService {
}
}
+ /**
+ * Format date to API datetime string format
+ * @param date Date to format
+ * @returns Formatted string like "2024-02-21 14:30:45"
+ */
+ private formatDateTime(date: Date): string {
+ const year = date.getFullYear();
+ const month = String(date.getMonth() + 1).padStart(2, '0');
+ const day = String(date.getDate()).padStart(2, '0');
+ const hours = String(date.getHours()).padStart(2, '0');
+ const minutes = String(date.getMinutes()).padStart(2, '0');
+ const seconds = String(date.getSeconds()).padStart(2, '0');
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+ }
+
+ /**
+ * Format a human-readable window name from unit and number
+ * @param unit 3=hour(s), 5=month(s), 6=week(s)
+ * @param number Quantity of the time unit
+ * @returns Formatted string like "5-Hour", "1-Week", "1-Month"
+ */
+ private formatWindowName(unit: number, number: number): string {
+ const unitNames: { [key: number]: string } = {
+ 3: 'Hour',
+ 5: 'Month',
+ 6: 'Week'
+ };
+ const unitName = unitNames[unit] || 'Unknown';
+ const plural = number > 1 ? 's' : '';
+ return `${number}-${unitName}${plural}`;
+ }
+
/**
* Fetch data from a specific endpoint
* Tries both Bearer and direct token formats
@@ -210,13 +345,6 @@ export class ZaiService {
this.apiKey = apiKey;
}
- /**
- * Update plan limit
- */
- updatePlanLimit(planLimit: number): void {
- this.planLimit = planLimit;
- }
-
/**
* Debug: Fetch and return raw API responses to see what data is available
*/
@@ -231,18 +359,8 @@ export class ZaiService {
const start7d = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, 0, 0, 0, 0);
const end7d = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999);
- const formatDateTime = (date: Date): string => {
- const year = date.getFullYear();
- const month = String(date.getMonth() + 1).padStart(2, '0');
- const day = String(date.getDate()).padStart(2, '0');
- const hours = String(date.getHours()).padStart(2, '0');
- const minutes = String(date.getMinutes()).padStart(2, '0');
- const seconds = String(date.getSeconds()).padStart(2, '0');
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
- };
-
- const queryParams24h = `?startTime=${encodeURIComponent(formatDateTime(start24h))}&endTime=${encodeURIComponent(formatDateTime(end24h))}`;
- const queryParams7d = `?startTime=${encodeURIComponent(formatDateTime(start7d))}&endTime=${encodeURIComponent(formatDateTime(end7d))}`;
+ const queryParams24h = `?startTime=${encodeURIComponent(this.formatDateTime(start24h))}&endTime=${encodeURIComponent(this.formatDateTime(end24h))}`;
+ const queryParams7d = `?startTime=${encodeURIComponent(this.formatDateTime(start7d))}&endTime=${encodeURIComponent(this.formatDateTime(end7d))}`;
const results = {
quotaLimit: null as any,
diff --git a/src/config/configuration.ts b/src/config/configuration.ts
index 1a2abac..775fd64 100644
--- a/src/config/configuration.ts
+++ b/src/config/configuration.ts
@@ -3,31 +3,19 @@ import * as vscode from 'vscode';
export interface ZaiConfiguration {
/** @deprecated API key is now stored in SecretStorage. This field is only for migration. */
apiKey: string;
- planTier: 'lite' | 'pro' | 'max';
refreshInterval: number;
}
-export const PLAN_LIMITS: Record = {
- lite: 120,
- pro: 600,
- max: 2400
-};
-
export function getConfiguration(): ZaiConfiguration {
const config = vscode.workspace.getConfiguration('zaiUsage');
return {
// Note: apiKey is deprecated - only read for migration from old versions
// New keys are stored in VS Code SecretStorage
apiKey: config.get('apiKey', ''),
- planTier: config.get<'lite' | 'pro' | 'max'>('planTier', 'lite'),
refreshInterval: config.get('refreshInterval', 5)
};
}
-export function getPlanLimit(tier: 'lite' | 'pro' | 'max'): number {
- return PLAN_LIMITS[tier] || PLAN_LIMITS.lite;
-}
-
export async function updateConfiguration(key: string, value: any): Promise {
const config = vscode.workspace.getConfiguration('zaiUsage');
await config.update(key, value, vscode.ConfigurationTarget.Global);
diff --git a/src/extension.ts b/src/extension.ts
index fd76aa5..bfe502e 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import { ZaiService, UsageData } from './api/zaiService';
import { UsageIndicator } from './statusBar/usageIndicator';
-import { getConfiguration, getPlanLimit } from './config/configuration';
+import { getConfiguration } from './config/configuration';
let zaiService: ZaiService | null = null;
let usageIndicator: UsageIndicator | null = null;
@@ -13,7 +13,7 @@ export function activate(context: vscode.ExtensionContext) {
// Create status bar indicator immediately (always visible)
if (!usageIndicator) {
- zaiService = new ZaiService('', getPlanLimit('lite'));
+ zaiService = new ZaiService('');
usageIndicator = new UsageIndicator(zaiService);
usageIndicator.showNotConfigured();
}
@@ -150,14 +150,11 @@ async function initializeService(context: vscode.ExtensionContext, apiKey?: stri
return;
}
- const planLimit = getPlanLimit(config.planTier);
-
// Update existing service or create new one
if (zaiService) {
zaiService.updateApiKey(effectiveApiKey);
- zaiService.updatePlanLimit(planLimit);
} else {
- zaiService = new ZaiService(effectiveApiKey, planLimit);
+ zaiService = new ZaiService(effectiveApiKey);
}
// Update indicator
@@ -257,11 +254,6 @@ async function configureSettings(context: vscode.ExtensionContext): Promise {
- if (selection === 'Configure Plan Tier') {
- await promptPlanTier(context);
- } else if (selection === 'Test Connection') {
+ if (selection === 'Test Connection') {
await initializeService(context);
refreshUsage();
}
@@ -339,25 +328,6 @@ async function configureSettings(context: vscode.ExtensionContext): Promise {
}
}
-/**
- * Prompt user to select plan tier
- */
-async function promptPlanTier(context: vscode.ExtensionContext): Promise {
- const config = getConfiguration();
-
- const tier = await vscode.window.showQuickPick([
- { label: 'Lite', description: '~120 prompts every 5 hours' },
- { label: 'Pro', description: '~600 prompts every 5 hours' },
- { label: 'Max', description: '~2400 prompts every 5 hours' }
- ], {
- placeHolder: 'Select your GLM Coding Plan tier',
- canPickMany: false
- });
-
- if (tier) {
- await vscode.workspace.getConfiguration('zaiUsage').update(
- 'planTier',
- tier.label.toLowerCase(),
- vscode.ConfigurationTarget.Global
- );
- vscode.window.showWarningMessage(`✓ Plan tier set to ${tier.label}!`);
- await initializeService(context);
- refreshUsage();
- }
-}
-
export function deactivate() {
console.log('Z.ai Usage Tracker extension is now deactivated');
diff --git a/src/statusBar/usageIndicator.ts b/src/statusBar/usageIndicator.ts
index 67e88dc..b4b4bd6 100644
--- a/src/statusBar/usageIndicator.ts
+++ b/src/statusBar/usageIndicator.ts
@@ -23,16 +23,40 @@ export class UsageIndicator {
this.currentError = null;
// Determine connection status icon
- const connectionIcon = usage.connectionStatus === 'connected' ? '✓' : '⚠';
+ const connectionIcon = usage.connectionStatus === 'connected' ? '' : '$(warning)';
+
+ // Find the quota with the longest time window to display in status bar
+ // Only consider token quotas (TOKENS_LIMIT type), not tool limits (TIME_LIMIT)
+ // Priority: Month (5) > Week (6) > Hour (3), then compare number
+ const tokenQuotas = usage.tokenQuotas;
+ let displayQuota = tokenQuotas.length > 0 ? tokenQuotas[0] : null;
+ if (tokenQuotas.length > 1) {
+ displayQuota = tokenQuotas.reduce((max, quota) => {
+ const maxPriority = this.getTimeWindowPriority(max.unit, max.number);
+ const quotaPriority = this.getTimeWindowPriority(quota.unit, quota.number);
+ return quotaPriority > maxPriority ? quota : max;
+ });
+ }
+
+ // Format the display text
+ let text: string;
+ if (displayQuota) {
+ let usageInfo = '';
+ if (displayQuota.actualTokens && displayQuota.percentage > 0) {
+ const estimatedLimit = Math.round(displayQuota.actualTokens / (displayQuota.percentage / 100));
+ usageInfo = ` · ${this.formatNumber(displayQuota.actualTokens)} / ${this.formatNumber(estimatedLimit)} Tokens`;
+ }
+ text = `$(zap) GLM: ${connectionIcon} ${displayQuota.percentage}%${usageInfo}`;
+ } else {
+ text = `$(zap) GLM: ${connectionIcon} No quota data`;
+ }
- // Format the display text: show 5-hour token quota percentage and current tokens
- const tokenDisplay = this.formatNumber(usage.current5HourTokens);
- const text = `${connectionIcon} $(zap) ${usage.percentage5Hour}% • ${tokenDisplay} tokens`;
this.statusBarItem.text = text;
this.statusBarItem.tooltip = this.getTooltip();
- // Set background color based on usage
- if (usage.percentage5Hour >= 80) {
+ // Set background color based on highest usage
+ const maxPercentage = displayQuota?.percentage || 0;
+ if (maxPercentage >= 80) {
this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
} else {
this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.prominentBackground');
@@ -45,7 +69,15 @@ export class UsageIndicator {
showError(error: string): void {
this.currentError = error;
this.statusBarItem.text = `$(error) Error`;
- this.statusBarItem.tooltip = `Error fetching Z.ai usage: ${error}\nClick to refresh`;
+
+ const md = new vscode.MarkdownString();
+ md.isTrusted = true;
+ md.supportThemeIcons = true; // Enable Codicon icons
+ md.appendMarkdown('## $(error) Error Fetching Usage\n\n');
+ md.appendMarkdown(`**Error**: \`${error}\`\n\n`);
+ md.appendMarkdown('[$(refresh) Click to Refresh](command:zaiUsage.refresh)');
+
+ this.statusBarItem.tooltip = md;
this.statusBarItem.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
}
@@ -76,11 +108,21 @@ export class UsageIndicator {
/**
* Get simple tooltip for status bar hover
*/
- private getSimpleTooltip(): string {
- if (!this.currentUsage) {
+ private getSimpleTooltip(): vscode.MarkdownString | string {
+ if (!this.currentUsage || this.currentUsage.tokenQuotas.length === 0) {
return 'Click to view Z.ai usage details';
}
- return `Z.ai Usage: ${this.currentUsage.percentage5Hour}% of 5-hour quota\nClick to view details`;
+ const md = new vscode.MarkdownString();
+ md.isTrusted = true;
+ md.supportHtml = true;
+ md.supportThemeIcons = true; // Enable Codicon icons
+
+ const quotaSummary = this.currentUsage.tokenQuotas
+ .map(q => `**${q.windowName}**: \`${q.percentage}%\``)
+ .join(' • ');
+ md.appendMarkdown(`$(zap) **Z.ai Usage**: ${quotaSummary}\n\n`);
+ md.appendMarkdown('*Click to view details*');
+ return md;
}
/**
@@ -93,42 +135,132 @@ export class UsageIndicator {
/**
* Get tooltip with detailed information
*/
- private getTooltip(): string {
+ private getTooltip(): vscode.MarkdownString | string {
if (!this.currentUsage) {
return 'Click for options';
}
const lastUpdated = this.formatDate(this.currentUsage.lastUpdated);
+ const md = new vscode.MarkdownString();
+ md.isTrusted = true;
+ md.supportHtml = true;
+ md.supportThemeIcons = true; // Enable Codicon icons
- let tooltip = '⚡ Z.ai GLM Usage\n';
- tooltip += '─────────────────\n\n';
-
- // 5-Hour Token Quota
- tooltip += `📊 5-Hour Quota (${this.currentUsage.percentage5Hour}%)\n`;
- tooltip += ` ${this.formatNumber(this.currentUsage.current5HourTokens)} / ${this.formatNumber(this.currentUsage.limit5HourTokens)} tokens\n`;
- tooltip += ` ${this.getProgressBar(this.currentUsage.percentage5Hour)}\n\n`;
-
- // 7-Day Stats
- tooltip += `📅 Last 7 Days\n`;
- tooltip += ` ${this.currentUsage.sevenDayPrompts} prompts • ${this.formatNumber(this.currentUsage.sevenDayTokens)} tokens\n\n`;
-
- // 30-Day Stats (All Time)
- tooltip += `📆 Last 30 Days\n`;
- tooltip += ` ${this.currentUsage.thirtyDayPrompts} prompts • ${this.formatNumber(this.currentUsage.thirtyDayTokens)} tokens\n\n`;
+ // Header
+ md.appendMarkdown('#### $(zap) Z.AI GLM Coding Plan Usage\n');
// Connection status
+ let statusIcon = '';
+ let statusText = '';
if (this.currentUsage.connectionStatus === 'connected') {
- tooltip += `✓ Connected`;
+ statusIcon = '$(check)';
+ statusText = 'Connected';
} else if (this.currentUsage.connectionStatus === 'disconnected') {
- tooltip += `⚠ Offline`;
+ statusIcon = '$(warning)';
+ statusText = 'Offline';
} else {
- tooltip += `✗ Error`;
+ statusIcon = '$(error)';
+ statusText = 'Error';
}
- tooltip += ` • Updated ${lastUpdated}\n\n`;
- tooltip += 'Click for more options';
+ // Build status line with plan level
+ let statusLine = `${statusIcon} *${statusText}* $(clock) Updated *${lastUpdated}*`;
+ if (this.currentUsage.planLevel) {
+ const planLevelDisplay = `GLM Coding Plan **${this.currentUsage.planLevel.charAt(0).toUpperCase() + this.currentUsage.planLevel.slice(1)}**`;
+ statusLine += ` $(star) *${planLevelDisplay}*`;
+ }
+ md.appendMarkdown(`${statusLine}\n\n`);
+
+ md.appendMarkdown('---\n');
+ md.appendMarkdown('$(graph) *Plan Quotas*\n\n');
+ md.appendMarkdown('---\n');
+
+ // Token Quota Windows (dynamic - display all from API)
+ if (this.currentUsage.tokenQuotas.length > 0) {
+
+ // Build compact Markdown table (4 columns, no header)
+ md.appendMarkdown('| | | | |\n');
+ md.appendMarkdown('|:--------|------:|:--------:|:----------|\n');
+
+ for (const quota of this.currentUsage.tokenQuotas) {
+ const resetInfo = quota.nextResetTime
+ ? `Reset ${this.formatResetTime(quota.nextResetTime)}`
+ : 'No reset time';
+ const progressBar = this.getProgressBar(quota.percentage);
+ const quotaLabel = `${quota.windowName}`;
+ const percentageText = `${quota.percentage}%`;
+
+ md.appendMarkdown(`| ${quotaLabel} | ${percentageText} | ${progressBar} | *${resetInfo}* |\n`);
+ }
+
+ // Show estimated token limits based on usage data
+ const estimatedLimits = this.calculateEstimatedLimits();
+ if (estimatedLimits) {
+ md.appendMarkdown('\n');
+ md.appendMarkdown(`$(info) *Estimated limits: ${estimatedLimits}*\n\n`);
+ md.appendMarkdown('$(warning) *Note: Estimated based on usage % and used tokens, not official limits*\n\n');
+ }
+ } else {
+ md.appendMarkdown('$(info) *No Quota Data Available*\n\n');
+ }
- return tooltip;
+ md.appendMarkdown('---\n\n');
+
+ // Usage Stats Table
+ md.appendMarkdown('$(graph) *Usage Statistics*\n\n');
+ md.appendMarkdown('---\n');
+ md.appendMarkdown('| | | | | |\n');
+ md.appendMarkdown('|:-------|--------:|:--------:|--------:|:--------:|\n');
+ md.appendMarkdown(`| $(calendar) Today | **${this.currentUsage.todayPrompts}** | Prompts | **${this.formatNumber(this.currentUsage.todayTokens)}** | Tokens |\n`);
+ md.appendMarkdown(`| $(calendar) Weeks | **${this.currentUsage.sevenDayPrompts}** | Prompts | **${this.formatNumber(this.currentUsage.sevenDayTokens)}** | Tokens |\n`);
+ md.appendMarkdown(`| $(calendar) Months | **${this.currentUsage.thirtyDayPrompts}** | Prompts | **${this.formatNumber(this.currentUsage.thirtyDayTokens)}** | Tokens |\n`);
+ md.appendMarkdown('\n');
+ md.appendMarkdown('$(info) *Prompts = Model invocations (each user prompt may trigger 10-20+ calls)*\n\n');
+
+ // MCP Tool Limits
+ if (this.currentUsage.timeLimits.length > 0) {
+ md.appendMarkdown('---\n');
+ md.appendMarkdown('$(tools) *MCP Tool Quotas*\n\n');
+ md.appendMarkdown('---\n');
+ md.appendMarkdown('| | | |\n');
+ md.appendMarkdown('|------:|:--------:|:----------:|\n');
+
+ for (const timeLimit of this.currentUsage.timeLimits) {
+ const resetInfo = timeLimit.nextResetTime
+ ? `Reset ${this.formatResetTime(timeLimit.nextResetTime)}`
+ : 'No reset time';
+ const progressBar = this.getProgressBar(timeLimit.percentage);
+ const usageText = `${timeLimit.currentValue}/${timeLimit.usage}`;
+
+ md.appendMarkdown(`| ${usageText} | ${progressBar} | *${resetInfo}* |\n`);
+ }
+
+ }
+
+ md.appendMarkdown('---\n\n');
+
+ // Action links
+ md.appendMarkdown('[$(refresh) Refresh](command:zaiUsage.refresh "Fetch latest usage data") ');
+ md.appendMarkdown('[$(gear) Settings](command:zaiUsage.configure "Configure API key and settings")');
+
+ return md;
+ }
+
+ /**
+ * Get priority for time window comparison
+ * Higher priority = longer time period
+ * @param unit 3=hour(s), 5=month(s), 6=week(s)
+ * @param number Quantity of the time unit
+ * @returns Priority value (higher = longer period)
+ */
+ private getTimeWindowPriority(unit: number, number: number): number {
+ // Priority: Month > Week > Hour
+ const unitPriorities: { [key: number]: number } = {
+ 5: 100000, // Month
+ 6: 10000, // Week
+ 3: 1000 // Hour
+ };
+ return (unitPriorities[unit] || 0) + number;
}
/**
@@ -145,7 +277,7 @@ export class UsageIndicator {
}
/**
- * Format date for display
+ * Format date for display (for past times)
*/
private formatDate(date: Date): string {
const now = new Date();
@@ -163,47 +295,87 @@ export class UsageIndicator {
}
}
+ /**
+ * Format reset time for display (for future times)
+ * Shows precise time with minutes for quota windows (e.g., 5-hour, 7-day limits)
+ */
+ private formatResetTime(timestamp: number): string {
+ const resetDate = new Date(timestamp);
+ const now = new Date();
+ const diffMs = resetDate.getTime() - now.getTime();
+
+ // Calculate time components
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMs / 3600000);
+ const diffDays = Math.floor(diffMs / 86400000);
+
+ // Remaining minutes after removing full hours
+ const remainingMins = diffMins % 60;
+ // Remaining hours after removing full days
+ const remainingHours = diffHours % 24;
+
+ if (diffMins < 1) {
+ return 'Soon';
+ } else if (diffMins < 60) {
+ // Less than 1 hour: show minutes only
+ return `in ${diffMins}m`;
+ } else if (diffHours < 24) {
+ // Less than 1 day: show hours + minutes
+ return `in ${diffHours}h ${remainingMins}m`;
+ } else if (diffDays < 7) {
+ // Less than 1 week: show days + hours + minutes
+ return `in ${diffDays}d ${remainingHours}h ${remainingMins}m`;
+ } else {
+ // For longer periods, show the actual date and time in numeric format
+ const year = resetDate.getFullYear();
+ const month = String(resetDate.getMonth() + 1).padStart(2, '0');
+ const day = String(resetDate.getDate()).padStart(2, '0');
+ const hours = String(resetDate.getHours()).padStart(2, '0');
+ const minutes = String(resetDate.getMinutes()).padStart(2, '0');
+ return `${year}-${month}-${day} ${hours}:${minutes}`;
+ }
+ }
+
/**
* Get text progress bar
*/
private getProgressBar(percentage: number): string {
const totalBars = 20;
const filledBars = Math.round((percentage / 100) * totalBars);
- return '█'.repeat(filledBars) + '░'.repeat(totalBars - filledBars);
+ return '' + '█'.repeat(filledBars) + '░'.repeat(totalBars - filledBars) + '';
}
/**
- * Show quick pick menu with stats and actions
+ * Calculate estimated token limits based on usage percentage and actual usage
*/
- async showQuickPick(): Promise {
- const options: vscode.QuickPickItem[] = [];
-
- // Add stats section if we have usage data
- if (this.currentUsage) {
- options.push({
- label: `$(graph) 5-Hour Quota: ${this.currentUsage.percentage5Hour}%`,
- description: `${this.formatNumber(this.currentUsage.current5HourTokens)} / ${this.formatNumber(this.currentUsage.limit5HourTokens)} tokens`,
- kind: vscode.QuickPickItemKind.Default
- });
+ private calculateEstimatedLimits(): string | null {
+ if (!this.currentUsage || this.currentUsage.tokenQuotas.length === 0) {
+ return null;
+ }
- options.push({
- label: `$(calendar) Last 7 Days`,
- description: `${this.currentUsage.sevenDayPrompts} prompts • ${this.formatNumber(this.currentUsage.sevenDayTokens)} tokens`
- });
+ const estimates: string[] = [];
- options.push({
- label: `$(history) Last 30 Days`,
- description: `${this.currentUsage.thirtyDayPrompts} prompts • ${this.formatNumber(this.currentUsage.thirtyDayTokens)} tokens`
- });
+ for (const quota of this.currentUsage.tokenQuotas) {
+ // Skip if percentage is 0 to avoid division by zero, or if we don't have actual tokens
+ if (quota.percentage === 0 || !quota.actualTokens || quota.actualTokens === 0) {
+ continue;
+ }
- // Separator
- options.push({
- label: '',
- kind: vscode.QuickPickItemKind.Separator
- });
+ // Calculate estimated total limit: actual / (percentage / 100)
+ const estimatedLimit = Math.round(quota.actualTokens / (quota.percentage / 100));
+ estimates.push(`${quota.windowName}: ${this.formatNumber(quota.actualTokens)}/${this.formatNumber(estimatedLimit)}`);
}
- // Action items
+ return estimates.length > 0 ? estimates.join(' • ') : null;
+ }
+
+ /**
+ * Show quick pick menu with stats and actions
+ */
+ async showQuickPick(): Promise {
+ const options: vscode.QuickPickItem[] = [];
+
+ // Action items only - detailed stats are in the tooltip
options.push({
label: '$(refresh) Refresh Usage',
description: 'Fetch latest usage data'
@@ -211,11 +383,11 @@ export class UsageIndicator {
options.push({
label: '$(settings-gear) Configure Settings',
- description: 'Update API key and plan tier'
+ description: 'Update API key and refresh interval'
});
const selected = await vscode.window.showQuickPick(options, {
- placeHolder: 'Z.ai Usage Tracker'
+ placeHolder: 'Z.ai Usage Tracker - Select an action'
});
if (selected?.label.includes('Refresh')) {