diff --git a/AGENTS.md b/AGENTS.md index daaaf76..027e0e2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -245,7 +245,23 @@ async function getCredentials(): Promise { } ### Development Principles -**CRITICAL:** Implementation MUST follow these principles to avoid over-engineering and maintain simplicity. +**CRITICAL:** Implementation MUST follow these principles to avoid over-engineering and maintain simplicity. Violating any of them is a blocking issue — stop and simplify before proceeding. + +**No Over-Thinking (Primary Rule):** +- Do NOT deliberate extensively on simple tasks — act decisively +- Do NOT speculate about future needs — solve the problem in front of you +- Do NOT create design documents for single-function changes +- Do NOT weigh alternatives for more than a few minutes — pick the simplest viable option and implement +- If a senior engineer would say "just do it", then just do it +- Over-thinking wastes time; over-engineering wastes code. Both are blockers. + +**KISS (Keep It Simple, Stupid):** +- Simplest correct solution wins. Always. +- Prefer plain functions over classes, factories, or patterns +- Prefer constants over config objects +- Prefer inline code over abstractions for single-use cases +- If code needs a 5-line comment to explain WHY it exists, it's too complex +- Fewer lines of code = fewer bugs = easier review **YAGNI (You Aren't Gonna Need It):** - Build only what's needed NOW, not what you might need LATER @@ -338,6 +354,28 @@ function isApiResponse(data: unknown): data is ApiResponse { - Simple code is easier to maintain - Flexible without requirements = complex for no reason +**TDD (Test-Driven Development) — MANDATORY for all code changes:** + +Workflow (Red → Green → Refactor): +1. **RED**: Write a failing test that describes the desired behavior BEFORE any implementation +2. **GREEN**: Write the minimum code to make the test pass — nothing more +3. **REFACTOR**: Improve code quality while keeping tests green + +Rules: +- ❌ NEVER write implementation before tests +- ❌ NEVER add code without a corresponding test +- ✅ One test at a time — write, run, repeat +- ✅ Tests define the spec; implementation satisfies it +- ✅ If a bug is found, write a test that reproduces it FIRST, then fix +- ✅ Commit only when all tests pass + +```bash +# TDD cycle commands +npm run test -- path/to/test.test.ts # Run specific test (RED check) +npm run test # Run all tests (GREEN verification) +npm run build # Ensure compiles +``` + **Reference:** - Implementation plan: `docs/implementation-plan.md` - Follows TDD with vertical slicing - ALL context needed for OpenAgents: `/home/eddy/distrobox/box-go-debian-home/.config/opencode/context/` - find the needed context here if you didnt found it in the project folder diff --git a/CHANGELOG.md b/CHANGELOG.md index 052fde1..a4d37e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). ## [Unreleased] +## [1.8.0] - 2026-06-15 + +### Added +- **Reset timestamp display in local timezone** - The "Resets In" column now shows the actual clock time alongside the countdown (Issue #34): + - Short durations: `4h 36m (01:34)` — countdown + local HH:MM + - Long durations (≥24h): `6d 16h (Sat 13:48)` — countdown + weekday + local HH:MM + - Uses native `Date` local time methods (`getHours()`, `getMinutes()`, `getDay()`) — no external dependencies, no unreliable timezone abbreviations + +### Changed +- `formatResetCell()` in `src/index.ts` now appends local reset time after the countdown portion + +### Technical +- Integration tests updated to use regex assertions (`assert.match`) for timezone-dependent output instead of exact string matching +- 117 tests passing (0 new tests, 2 updated assertions) +- Reported by @eagleii-dev (Pacific/Auckland, UTC+12) + ## [1.7.0] - 2026-04-02 ### Changed diff --git a/README.md b/README.md index 93ab5c7..a26b039 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ OpenCode plugin to query Z.ai GLM Coding Plan usage statistics with real-time qu - 🌍 Supports both Global (api.z.ai) and CN (open.bigmodel.cn) platforms - 🔐 Automatic credential discovery via OpenCode authentication context - 📈 Visual progress bars for quota percentages rendered in Markdown code spans -- ⏰ Reset countdown display - Shows when quota resets (hours/days) +- ⏰ Reset countdown display - Shows when quota resets with local timezone clock time (hours/days + HH:MM) - 🏷️ Account plan display - Shows your subscription tier (Lite, Pro, etc.) - ⚡ Fail-fast error handling (no retry logic - user controls when to retry) @@ -137,8 +137,8 @@ After authentication, simply run: | Window | Usage | Progress | Resets In | |--------|------:|----------|-----------| -| ⏱️ 5h Token | 40.5% | `█████░░░░░░░` | 3h 42m | -| 📅 Weekly | 52.0% | `██████░░░░░░` | 4d 12h | +| ⏱️ 5h Token | 40.5% | `█████░░░░░░░` | 3h 42m (01:34) | +| 📅 Weekly | 52.0% | `██████░░░░░░` | 4d 12h (Sat 13:48) | | 🔌 MCP (1 Month) | 12.3% | `█░░░░░░░░░░░` | — | ##### 📊 Quota Usage diff --git a/docs/implementation-plan-v1.8.0.md b/docs/implementation-plan-v1.8.0.md new file mode 100644 index 0000000..f792bdc --- /dev/null +++ b/docs/implementation-plan-v1.8.0.md @@ -0,0 +1,167 @@ +# Implementation Plan: v1.8.0 Reset Timestamp Display (Local Timezone) + +**Version:** 1.0 +**Date:** June 15, 2026 +**Branch:** `fix/reset-timer-timezone-34` +**Target Version:** 1.8.0 +**Issue:** [#34 — Display actual reset timestamp in local timezone](https://github.com/guyinwonder168/opencode-glm-quota/issues/34) +**PRD Reference:** `docs/opencode-glm-quota-prd-final.md` Section 1.12 + +--- + +## Goal + +Show the actual reset clock time in the user's local timezone alongside the existing countdown, so users in far-from-UTC zones can plan around resets without guessing when they occur. + +## Problem + +**Before:** `| ⏱️ 5h Token | 87.0% | ████████████████░░░░░ | 4h 36m |` +**After:** `| ⏱️ 5h Token | 87.0% | ████████████████░░░░░ | 4h 36m (01:34) |` + +For long durations (≥24h), also show the weekday: +`| 📅 Weekly | 17.0% | ████░░░░░░░░░░░░░░░░ | 6d 16h (Sat 13:48) |` + +## Design Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Timezone display format | `HH:MM` (local clock time) | Deterministic, user knows their own timezone | +| Timezone abbreviation | **Not included** | Node.js `toLocaleTimeString` TZ abbreviations are unreliable across platforms (e.g., "GMT+13" vs "NZST") | +| Long duration format | `Day HH:MM` (e.g., `Sat 13:48`) | Matches issue's proposed format for weekly resets | +| API for local time | `getHours()`, `getMinutes()`, `getDay()` | Follows existing `date-formatter.ts` pattern, no dependencies | +| Testing approach | Regex assertions in integration tests | Timezone-dependent output made deterministic via pattern matching | + +## Impact Analysis + +### Files MODIFIED + +| File | Changes | +|------|---------| +| `src/index.ts` | `formatResetCell()` — add local time display after countdown | +| `tests/integration/reset-time-display.test.ts` | Update assertions: `includes` → `match` with regex for time pattern | +| `package.json` | Version: `1.7.0` → `1.8.0` | +| `sonar-project.properties` | `sonar.projectVersion`: `1.7.0` → `1.8.0` | +| `CHANGELOG.md` | Add `[1.8.0]` entry | +| `README.md` | Update feature bullet + output example | +| `docs/opencode-glm-quota-prd-final.md` | Add Section 1.12 | + +### Files UNCHANGED + +| File | Reason | +|------|--------| +| `src/utils/reset-timer.ts` | Long-form `formatTimeUntilReset()` not used in Markdown table, separate concern | +| All other `src/` files | No output or logic changes needed | + +--- + +## Implementation Slices + +### Slice 1: TDD RED — Update integration test assertions + +**Goal:** Write failing tests that describe the desired new format. + +**Changes to `tests/integration/reset-time-display.test.ts`:** +- Short duration test: `includes('| ... | 4h 42m |')` → `match(/\| ... \| 4h 42m \(\d{2}:\d{2}\) \|/)` +- Long duration test: `includes('| ... | 4d 12h |')` → `match(/\| ... \| 4d 12h \([A-Z][a-z]{2} \d{2}:\d{2}\) \|/)` +- MCP test: Unchanged (no reset time → still `—`) + +**Verify:** `npm run test -- tests/integration/reset-time-display.test.ts` → 2 failures (RED) + +--- + +### Slice 2: TDD GREEN — Modify `formatResetCell()` + +**Goal:** Minimum code to make tests pass. + +**Changes to `src/index.ts` `formatResetCell()` (was lines 283-301):** + +```typescript +function formatResetCell(resetTime?: number): string { + const resetAt = asNumber(resetTime); + if (resetAt === null) return '—'; + + const diffMs = resetAt - Date.now(); + if (diffMs <= 0) return '—'; + + const totalMinutes = Math.floor(diffMs / (1000 * 60)); + + // Format countdown portion + let countdown: string; + if (totalMinutes >= 24 * 60) { + const totalHours = Math.floor(totalMinutes / 60); + countdown = `${Math.floor(totalHours / 24)}d ${totalHours % 24}h`; + } else { + countdown = `${Math.floor(totalMinutes / 60)}h ${totalMinutes % 60}m`; + } + + // Format local reset time portion (Issue #34) + const resetDate = new Date(resetAt); + const hh = String(resetDate.getHours()).padStart(2, '0'); + const mm = String(resetDate.getMinutes()).padStart(2, '0'); + + if (totalMinutes >= 24 * 60) { + const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return `${countdown} (${dayNames[resetDate.getDay()]} ${hh}:${mm})`; + } + + return `${countdown} (${hh}:${mm})`; +} +``` + +**Verify:** `npm run test -- tests/integration/reset-time-display.test.ts` → all pass (GREEN) + +--- + +### Slice 3: Full test suite verification + +**Goal:** Ensure no regressions. + +**Verify:** `npm test` → 117 tests pass, 0 failures + +--- + +### Slice 4: Documentation + version bump + +**Tasks:** +1. `package.json`: `1.7.0` → `1.8.0` +2. `sonar-project.properties`: `sonar.projectVersion` `1.7.0` → `1.8.0` +3. `CHANGELOG.md`: Add `[1.8.0]` entry under `[Unreleased]` +4. `README.md`: Update feature bullet + output example +5. `docs/opencode-glm-quota-prd-final.md`: Add Section 1.12 + +**Verify:** `npm run build && npm run lint && npm test` all pass + +--- + +## Execution Order + +``` +Slice 1 (RED tests) + ↓ +Slice 2 (GREEN implementation) + ↓ +Slice 3 (full suite verification) + ↓ +Slice 4 (docs + version bump) +``` + +All slices are sequential — small scope, no parallelism needed. + +## Risk Assessment + +| Risk | Mitigation | +|------|------------| +| Timezone-dependent test output | Regex assertions (`\d{2}:\d{2}`) instead of hardcoded times | +| Cell width increase | "Resets In" column is last, Markdown tables auto-size — no alignment issues | +| DST transitions | `getHours()`/`getMinutes()` always return local time per system TZ — correct by design | +| Node.js version differences | Using standard `Date` methods available since ES1 — universally supported | + +## Estimated Effort + +| Slice | Files | Time | +|-------|-------|------| +| Slice 1 | 1 | 10 min | +| Slice 2 | 1 | 10 min | +| Slice 3 | 0 | 5 min | +| Slice 4 | 5 | 20 min | +| **Total** | **7 files** | **~45 min** | diff --git a/docs/opencode-glm-quota-prd-final.md b/docs/opencode-glm-quota-prd-final.md index ecb0d57..9575623 100644 --- a/docs/opencode-glm-quota-prd-final.md +++ b/docs/opencode-glm-quota-prd-final.md @@ -428,6 +428,47 @@ Your token was rejected by the API. --- +### 1.12 Reset Timestamp Display in Local Timezone (COMPLETED - v1.8.0 ✅) + +**Problem Statement:** + +The reset timer only shows a **countdown** (`4h 36m`), but not the **actual reset timestamp**. For users in timezones far from UTC (e.g., UTC+12 Pacific/Auckland), the 5-hour rolling window reset often happens at an inconvenient local time (e.g., 1–2 AM), and it's not obvious *when* exactly the quota will reset (Issue #34). + +**Solution:** + +Show the actual reset time in the user's local timezone alongside the countdown: + +``` +| ⏱️ 5h Token | 87.0% | ████████████████░░░░░ | 4h 36m (01:34) | +| 📅 Weekly | 17.0% | ████░░░░░░░░░░░░░░░░ | 6d 16h (Sat 13:48) | +``` + +**Design Decisions:** + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Timezone display format | `HH:MM` local clock time | Deterministic, user knows their own timezone | +| Timezone abbreviation | Not included | Node.js `toLocaleTimeString` TZ abbreviations unreliable across platforms | +| Long duration format | `Day HH:MM` | Matches issue's proposed format for weekly resets | +| API | `Date.getHours()`, `getMinutes()`, `getDay()` | Native methods, no dependencies, follows existing `date-formatter.ts` pattern | + +**Implementation:** + +- `formatResetCell()` in `src/index.ts` computes local time from `nextResetTime` Unix timestamp +- Short durations (`<24h`): Returns `${countdown} (${HH}:${MM})` +- Long durations (`≥24h`): Returns `${countdown} (${DayName} ${HH}:${MM})` +- MCP rows without `nextResetTime`: Unchanged (`—`) + +**Testing:** + +- Integration tests use regex assertions (`assert.match`) for timezone-dependent output +- Pattern: `/\d{2}:\d{2}/` for short durations, `/[A-Z][a-z]{2} \d{2}:\d{2}/` for long durations +- Works deterministically across CI environments (GitHub Actions defaults to UTC) + +**Status:** ✅ COMPLETED (2026-06-15) - v1.8.0, resolves Issue #34 + +--- + ## 2. OpenCode Plugin Architecture ### 2.1 OpenCode Plugin System diff --git a/package.json b/package.json index 85ec587..01d91a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "opencode-glm-quota", - "version": "1.7.0", + "version": "1.8.0", "type": "module", "description": "OpenCode plugin to query Z.ai GLM Coding Plan usage statistics including quota limits, model usage, and MCP tool usage", "main": "dist/index.js", diff --git a/sonar-project.properties b/sonar-project.properties index 11baed8..cd98a43 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -8,7 +8,7 @@ sonar.host.url=https://sonarcloud.io # Project metadata sonar.projectName=OpenCode GLM Quota Plugin -sonar.projectVersion=1.7.0 +sonar.projectVersion=1.8.0 # Source code configuration sonar.sources=src diff --git a/src/index.ts b/src/index.ts index 81acbfc..f0fb0af 100644 --- a/src/index.ts +++ b/src/index.ts @@ -292,12 +292,27 @@ function formatResetCell(resetTime?: number): string { } const totalMinutes = Math.floor(diffMs / (1000 * 60)); + + // Format countdown portion + let countdown: string; if (totalMinutes >= 24 * 60) { const totalHours = Math.floor(totalMinutes / 60); - return `${Math.floor(totalHours / 24)}d ${totalHours % 24}h`; + countdown = `${Math.floor(totalHours / 24)}d ${totalHours % 24}h`; + } else { + countdown = `${Math.floor(totalMinutes / 60)}h ${totalMinutes % 60}m`; + } + + // Format local reset time portion (Issue #34: show actual clock time) + const resetDate = new Date(resetAt); + const hh = String(resetDate.getHours()).padStart(2, '0'); + const mm = String(resetDate.getMinutes()).padStart(2, '0'); + + if (totalMinutes >= 24 * 60) { + const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + return `${countdown} (${dayNames[resetDate.getDay()]} ${hh}:${mm})`; } - return `${Math.floor(totalMinutes / 60)}h ${totalMinutes % 60}m`; + return `${countdown} (${hh}:${mm})`; } function formatMarkdownTable(headers: string[], separator: string[], rows: string[][]): string { diff --git a/tests/integration/reset-time-display.test.ts b/tests/integration/reset-time-display.test.ts index 9869ab4..3dd5c26 100644 --- a/tests/integration/reset-time-display.test.ts +++ b/tests/integration/reset-time-display.test.ts @@ -70,7 +70,7 @@ describe('Reset Time Display Integration', () => { delete process.env.ZAI_API_KEY }) - test('renders short reset windows as h and m in the Markdown quota table', async () => { + test('renders short reset windows as countdown with local time in the Markdown quota table', async () => { https.request = createMockRequest({ '/quota/limit': { statusCode: 200, @@ -103,10 +103,11 @@ describe('Reset Time Display Integration', () => { const plugin = await GlmQuotaPlugin({} as unknown as PluginContext) const result = await (plugin.tool!.glm_quota as unknown as ToolExecutor).execute() - assert.ok(result.includes('| ⏱️ 5h Token | 45.0% | `█████░░░░░░░` | 4h 42m |')) + // Should show countdown + local time: "4h 42m (HH:MM)" + assert.match(result, /\| ⏱️ 5h Token \| 45\.0% \| `█████░░░░░░░` \| 4h 42m \(\d{2}:\d{2}\) \|/) }) - test('renders long reset windows as days and hours in the Markdown quota table', async () => { + test('renders long reset windows as countdown with day and local time in the Markdown quota table', async () => { https.request = createMockRequest({ '/quota/limit': { statusCode: 200, @@ -139,7 +140,8 @@ describe('Reset Time Display Integration', () => { const plugin = await GlmQuotaPlugin({} as unknown as PluginContext) const result = await (plugin.tool!.glm_quota as unknown as ToolExecutor).execute() - assert.ok(result.includes('| 📅 Weekly | 52.0% | `██████░░░░░░` | 4d 12h |')) + // Should show countdown + day name + local time: "4d 12h (Day HH:MM)" + assert.match(result, /\| 📅 Weekly \| 52\.0% \| `██████░░░░░░` \| 4d 12h \([A-Z][a-z]{2} \d{2}:\d{2}\) \|/) }) test('renders MCP rows without a reset countdown in the Markdown quota table', async () => {