From 592e532df757cf6de1e5e4db8b5386efe12776ae Mon Sep 17 00:00:00 2001 From: RECTOR Date: Wed, 9 Sep 2026 18:59:59 +0700 Subject: [PATCH 1/8] docs: zai reset-time design spec (absolute time joins countdown) --- .../specs/2026-09-09-zai-reset-time-design.md | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-09-zai-reset-time-design.md diff --git a/docs/superpowers/specs/2026-09-09-zai-reset-time-design.md b/docs/superpowers/specs/2026-09-09-zai-reset-time-design.md new file mode 100644 index 0000000..78ceb3e --- /dev/null +++ b/docs/superpowers/specs/2026-09-09-zai-reset-time-design.md @@ -0,0 +1,69 @@ +# zai reset-time — design (approved 2026-09-09) + +Goal: each zai quota window's parenthetical gains the absolute wall-clock reset time +alongside the existing countdown. Presentation-only — the data layer is untouched: +`nextResetTime` (ms-epoch UTC) is already parsed, validated finite, and polled +(`parseQuotaLimit`, `src/quota/zai.ts:44-68`; segment render, `windowSeg`, +`src/index.ts:172-183`). + +## Verified API facts (live probe, 2026-09-09) + +- Endpoint `GET https://api.z.ai/api/monitor/usage/quota/limit` (same key) returns + `data.limits[]` — unit 3 = 5-hour window, unit 6 = weekly — each with `nextResetTime`. +- **5HRS is rolling**: each cycle anchors at the first request after the previous + reset, so the wall-clock reset time shifts every cycle. Probe: reset 23:30 Jakarta + (window started 18:30); the "11:30" seen earlier was the previous cycle's reset. +- **7DAY**: epoch 1789160956993 → Sat 12 Sep 04:09 Jakarta. The console's "05:09" + is the same instant in UTC+8 (Asia/Shanghai frame). Epoch is authoritative; we + render machine-local (RECTOR = Jakarta, UTC+7). + +## UI (approved — option B, both signals kept) + +``` +󰚯 zai 5hrs 1%/4% (9m under · 4h 47m · 23:30) · 7DAY 75%/66% (15h 7m over · 2d 9h · Sep 12 04:09) +``` + +- Parenthetical per window: `pace · countdown · absolute`. +- Absolute rule: reset falling **today** (machine-local calendar) → `HH:MM` + (`23:30`); **any other day** → `Mon DD HH:MM` (`Sep 12 04:09`). No weekday + abbreviation — ambiguous when the weekly reset lands on the viewer's weekday. +- Timezone: machine-local, consistent with `infoLine`'s clock. Never the API's + Shanghai frame. +- All three parenthetical fragments render `dim`; only pace keeps its over/under + token (reset info is periphery, v0.4.7 convention). +- Accepted cost: line grows ~23 columns vs the current shape. + +## Code + +- `src/format.ts` — new pure `formatResetAbs(targetMs: number, now: number): string`: + - `targetMs <= now` → `"now"` (mirrors `formatReset`'s guard); + - same local calendar day as `now` → `HH:MM` via existing `formatClock`; + - otherwise → `Sep 12 04:09` via a hardcoded EN month table (deterministic, no + `Intl` comma/platform variance). + - `formatReset` (countdown) stays as-is. +- `src/index.ts` `windowSeg` — insert one fragment before the closing paren: + `` theme.fg("dim", ` · ${formatResetAbs(lim.nextResetTime, now)}`) ``. +- Stale-doc sweep: header example `src/index.ts:2`, the README widget example + (README.md:24), and `assets/hero.svg`'s embedded zai row (tspans at line 36 — + plain monospace ``, glyphs stay symbol refs) all gain the absolute times. + `dataflow.svg` labels only ("zai · provider-gated") — no line content, no change. +- Zero changes: API/poller (`quota/zai.ts`), adapters, `/sl` report, config schema. + +## Tests (`test/lines.test.ts`) + +- `formatResetAbs`: same-day → `23:30`; crosses midnight → `Sep 10 01:30`; past → + `now`. Timestamps built from local `new Date(y, m, d, hh, mm)` constructors at + DST-edge-free instants, so assertions hold in any CI timezone. +- `zaiLine` approved-format test: parenthetical now three fragments — pace (own + token), countdown `dim`, absolute `dim`. + +## Verification + +- `pnpm typecheck && pnpm test:run` green. +- Live tmux render of the real widget: 5HRS shows today's actual reset clock-time, + 7DAY shows `Sep 12 04:09` from live API data. + +## Non-goals + +- No countdown removal, no compact/adaptive variants, no config knob, no tz + override, no `/sl` change, no adapter or API change. From 0bc6beb3a63fa85c7ab6d956175b4daf0390ac87 Mon Sep 17 00:00:00 2001 From: RECTOR Date: Wed, 9 Sep 2026 19:10:08 +0700 Subject: [PATCH 2/8] docs: zai reset-time implementation plan --- .../plans/2026-09-09-zai-reset-time.md | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 docs/superpowers/plans/2026-09-09-zai-reset-time.md diff --git a/docs/superpowers/plans/2026-09-09-zai-reset-time.md b/docs/superpowers/plans/2026-09-09-zai-reset-time.md new file mode 100644 index 0000000..9987c59 --- /dev/null +++ b/docs/superpowers/plans/2026-09-09-zai-reset-time.md @@ -0,0 +1,280 @@ +# zai Reset-Time Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Each zai quota window's parenthetical gains the absolute wall-clock reset time after the existing countdown: `(9m under · 4h 47m · 23:30)`. + +**Architecture:** Presentation-only. `formatResetAbs` (new pure formatter in `src/format.ts`) turns the already-polled `nextResetTime` epoch into `HH:MM` (same local day) or `Mon DD HH:MM` (other day); `windowSeg` in `src/index.ts` renders it as one extra dim fragment. Data layer, poller, adapters, config: untouched. + +**Tech Stack:** TypeScript (raw `.ts`, no build), `node --test` via tsx-capable Node, pnpm. Spec: `docs/superpowers/specs/2026-09-09-zai-reset-time-design.md`. + +## Global Constraints + +- Branch: `feat/zai-reset-time` (exists; spec committed at 592e532). +- Parenthetical order per window: `pace · countdown · absolute` (spec UI section). +- Absolute rule: reset today (machine-local calendar) → `HH:MM`; otherwise → `Mon DD HH:MM` (EN month table, no weekday, no `Intl`). +- Past/now reset renders `now` (mirrors `formatReset`'s `remaining <= 0` guard). +- All three parenthetical fragments render `theme.fg("dim", …)`; only pace keeps its over/under token. `formatReset` itself is NOT modified. +- Machine-local timezone everywhere (same as `infoLine`'s clock) — never Asia/Shanghai. +- Tests must be timezone-safe: fixed `now` built with local `new Date(y, m, d, hh, mm)` at Sep 9–13 instants (DST-edge-free globally). +- Commits: one per task, conventional style (`feat:`/`chore:`), no AI attribution. +- Run commands: `pnpm test:run` (full suite), `node --test test/lines.test.ts` (single file), `pnpm typecheck`. + +--- + +### Task 1: `formatResetAbs` formatter (TDD) + +**Files:** +- Modify: `src/format.ts` (append after `formatReset`, which ends at line 44) +- Test: `test/lines.test.ts` (add import at line 5 area; add test after the `zaiLine` test ending line 66) + +**Interfaces:** +- Consumes: `formatClock(ts: number): string` (exists in `src/format.ts:20-23`, returns local `HH:MM`). +- Produces: `formatResetAbs(targetMs: number, now: number): string` exported from `src/format.ts` — Task 2 imports this exact name. + +- [ ] **Step 1: Write the failing test** + +In `test/lines.test.ts`, add to the imports (new line after line 9's `QuotaResult` import): + +```ts +import { formatResetAbs } from "../src/format.ts"; +``` + +Insert after line 66 (end of the `zaiLine` test): + +```ts +test("formatResetAbs: same-day clock, cross-day month-day, past → now", () => { + const now = new Date(2026, 8, 9, 10, 0).getTime(); // Wed 09 Sep 2026 10:00 local — DST-edge-free + assert.equal(formatResetAbs(now + HOUR, now), "11:00"); + assert.equal(formatResetAbs(now + 20 * HOUR, now), "Sep 10 06:00"); + assert.equal(formatResetAbs(now - 60_000, now), "now"); +}); +``` + +(`HOUR` is declared at `test/lines.test.ts:103`; test callbacks run after module evaluation, so the forward reference is safe.) + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node --test --test-name-pattern "formatResetAbs" test/lines.test.ts` +Expected: FAIL — `SyntaxError: The requested module '../src/format.ts' does not provide an export named 'formatResetAbs'`. + +- [ ] **Step 3: Implement** + +Append at end of `src/format.ts` (after `formatReset`'s closing brace, line 44): + +```ts +// v0.3.x absolute reset wall-clock (reset-time feature): same-day resets read as +// clock time (`23:30`); anything further out carries an EN month-day (`Sep 12 +// 04:09`) — no weekday abbreviation (ambiguous when the reset lands on the +// viewer's own weekday). Machine-local like infoLine's clock; hardcoded EN months +// keep rendering deterministic (no Intl comma/platform variance). +const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const; + +export function formatResetAbs(targetMs: number, now: number): string { + if (targetMs <= now) return "now"; + const t = new Date(targetMs); + const n = new Date(now); + const sameDay = + t.getFullYear() === n.getFullYear() && + t.getMonth() === n.getMonth() && + t.getDate() === n.getDate(); + return sameDay ? formatClock(targetMs) : `${MONTHS[t.getMonth()]} ${t.getDate()} ${formatClock(targetMs)}`; +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +Run: `node --test --test-name-pattern "formatResetAbs" test/lines.test.ts` +Expected: PASS (1 test). + +- [ ] **Step 5: Commit** + +```bash +git add src/format.ts test/lines.test.ts +git commit -m "feat: formatResetAbs — absolute local reset wall-clock (HH:MM / Mon DD HH:MM)" +``` + +--- + +### Task 2: `windowSeg` renders the absolute fragment (TDD) + +**Files:** +- Modify: `src/index.ts:17` (import), `src/index.ts:180-181` (fragment insert) +- Test: `test/lines.test.ts:50-66` (rewrite `zaiLine` test with a pinned `now`) + +**Interfaces:** +- Consumes: `formatResetAbs(targetMs: number, now: number): string` (Task 1). +- Produces: no signature changes. `zaiLine`'s output shape gains one dim fragment per window — Task 3's docs sweep pins the same shape. + +- [ ] **Step 1: Rewrite the failing `zaiLine` test** + +Replace the whole test block at `test/lines.test.ts:50-66` with (pinned `now` — the old `Date.now()` would make same-day vs cross-day flaky near midnight): + +```ts +test("zaiLine: percents keep heat; paren = pace · countdown · absolute; lowercase 5hrs label", () => { + const now = new Date(2026, 8, 9, 10, 0).getTime(); // Wed 09 Sep 2026 10:00 local — DST-edge-free + // 5h window, nextReset 1h out → 80% elapsed. Usage 16% vs 80% → pace 5h×(16−80)/100 = 3h 12m under. + const fiveHour = (percentage: number) => ({ unit: 1, number: 1, usage: 28000, currentValue: 7664, remaining: 20335, percentage, nextResetTime: now + HOUR }); + const data = (p5: number): QuotaResult => ({ tier: "pro", fiveHour: fiveHour(p5), weekly: null, fetchedAt: now }); + assert.ok(zaiLine(theme, data(16), now, sep).includes("5hrs 16%/80%")); + assert.ok(zaiLine(theme, data(16), now, sep).includes(" (3h 12m under · 1h 0m · 11:00)")); + // over pace (usage > elapsed): 85% vs 80% → 5h×5/100 = 15m over, warning token + assert.ok(zaiLine(theme, data(85), now, sep).includes("15m over")); + assert.ok(zaiLine(theme, data(76), now, sep).includes("5hrs 76%/80%")); + assert.ok(zaiLine(theme, data(93), now, sep).includes("5hrs 93%/80%")); + assert.ok(zaiLine(theme, data(105), now, sep).includes("5hrs 100%+/80%")); + const both: QuotaResult = { tier: "pro", fiveHour: fiveHour(16), weekly: { ...fiveHour(24), nextResetTime: now + 3 * DAY + 18 * HOUR }, fetchedAt: now }; + assert.ok(zaiLine(theme, both, now, sep).includes(" · ")); + assert.ok(zaiLine(theme, both, now, sep).includes(" · Sep 13 04:00")); // cross-day → month-day form + const none: QuotaResult = { tier: "pro", fiveHour: null, weekly: null, fetchedAt: now }; + assert.equal(zaiLine(theme, none, now, sep), " 󰚯 zai — no quota windows"); +}); +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `node --test --test-name-pattern "zaiLine" test/lines.test.ts` +Expected: FAIL — actual parenthetical lacks ` · 11:00`. + +- [ ] **Step 3: Implement** + +`src/index.ts:17` — extend the import: + +```ts +import { formatReset, formatResetAbs } from "./format.ts"; +``` + +`src/index.ts` — in `windowSeg`, insert one array element between the countdown element (line 180) and the closing-paren element (line 181), so the tail of the array reads: + +```ts + theme.fg("dim", " · "), + theme.fg("dim", formatReset(lim.nextResetTime, now)), + theme.fg("dim", ` · ${formatResetAbs(lim.nextResetTime, now)}`), + theme.fg("dim", ")"), +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `node --test test/lines.test.ts` +Expected: PASS — all tests in the file, including the rewritten `zaiLine` and Task 1's `formatResetAbs`. + +- [ ] **Step 5: Commit** + +```bash +git add src/index.ts test/lines.test.ts +git commit -m "feat: zai windows render absolute reset time after the countdown" +``` + +--- + +### Task 3: Stale-doc sweep — header comment, README row, hero SVG + +**Files:** +- Modify: `src/index.ts:2` (header example) +- Modify: `README.md:24` (zai row example) +- Modify: `assets/hero.svg` (canvas widen 866→980 + append two dim tspans on the zai row) + +**Interfaces:** +- Consumes: the rendered shape from Task 2 (`pace · countdown · absolute`). +- Produces: docs/hero that match the renderer exactly. + +**Width math (why the canvas widens):** the zai row grows to 101 chars ≈ 101 × 9px = 909px at 15px mono, ending at x≈959 — past the current 866 viewBox. New canvas 980 leaves a 21px right margin. Mock stays internally consistent: clock 08:15 + 3h 43m = 11:58; mock day (Sep 7, the approval date per `src/index.ts:1`) + 4d 19h = Sep 12 03:15. + +- [ ] **Step 1: Update the header example** + +Replace `src/index.ts:2` with: + +```ts +// 󰚯 zai 5hrs 16%/80% (1h 36m over · 3h 43m · 11:58) · 7DAY 24%/31% (1d 5h under · 4d 19h · Sep 12 03:15) ← provider-gated (zai only) +``` + +- [ ] **Step 2: Update the README zai row** + +Replace `README.md:24` with: + +```markdown +| **zai** | Quota pace per window: `LABEL usage%/elapsed% (pace · reset · absolute)` — e.g. `5hrs 16%/80% (1h 36m over · 3h 43m · 11:58)`. **Provider-gated**: renders only while the active model's provider is `zai` (or the provider is unreadable); vanishes entirely on other providers. | +``` + +- [ ] **Step 3: Widen the hero canvas** + +Six coordinated edits in `assets/hero.svg`: + +1. Line 1: `width="866" height="329" viewBox="0 0 866 329"` → `width="980" height="329" viewBox="0 0 980 329"` +2. Line 17: `3h 43m)` with: + +```svg +3h 43m · 11:58) +``` + +and replace `4d 19h)` with: + +```svg +4d 19h · Sep 12 03:15) +``` + +- [ ] **Step 5: Visual check of the hero** + +Run: rasterize `assets/hero.svg` (e.g. `read assets/hero.svg:img` in-session) and confirm: no clipping at the right edge, zai row shows both absolutes, titlebar/annotation centered, hairline spans the new width. + +- [ ] **Step 6: Commit** + +```bash +git add src/index.ts README.md assets/hero.svg +git commit -m "docs: sync header/README/hero with absolute reset times; widen hero canvas to 980" +``` + +--- + +### Task 4: Verification + release prep + +**Files:** +- Modify: `package.json:3` (version bump) +- Create then delete: `smoke-reset.ts` (throwaway live smoke, never committed) + +**Interfaces:** +- Consumes: everything above; `fetchQuota`, `zaiLine`, `loadLiveConfig`, `resolveZaiKey` from the existing modules. + +- [ ] **Step 1: Full suite + typecheck** + +Run: `pnpm typecheck && pnpm test:run` +Expected: exit 0; all tests pass (lines, zai-key, zai-extra, money). + +- [ ] **Step 2: Live smoke against the real API** + +Create `smoke-reset.ts` at repo root: + +```ts +import { fetchQuota } from "./src/quota/zai.ts"; +import { zaiLine, loadLiveConfig, resolveZaiKey, type SlTheme } from "./src/index.ts"; +const cfg = loadLiveConfig(); +const zk = await resolveZaiKey(null, cfg); +if (!zk) throw new Error("no zai key resolved"); +const data = await fetchQuota(zk.key); +if (!data) throw new Error("quota fetch failed"); +console.log(zaiLine({ fg: (_t: string, s: string) => s } as SlTheme, data, Date.now(), " · ")); +``` + +Run: `node smoke-reset.ts` +Expected: one line shaped ` 󰚯 zai 5hrs … (… · 23:30) · 7DAY … (… · Sep 12 04:09)` on 2026-09-09 (absolute times come from the live `nextResetTime` epochs verified in the spec; values drift as windows roll — the shape is the contract). Then `rm smoke-reset.ts`. + +- [ ] **Step 3: Version bump** + +`package.json:3`: `"version": "0.4.0"` → `"version": "0.5.0"` (minor: new rendered component, no breaking change). + +- [ ] **Step 4: Push and open PR** + +```bash +git push -u origin feat/zai-reset-time +gh pr create --fill --base main +``` + +PR body: link the spec `docs/superpowers/specs/2026-09-09-zai-reset-time-design.md`, note the hero canvas widen and the live-smoke output. Tag `v0.5.0` after merge is RECTOR's call (tag push triggers the release workflow). From e962d6241251cc58bdc01a6344ac211432fd84bc Mon Sep 17 00:00:00 2001 From: RECTOR Date: Wed, 9 Sep 2026 19:50:56 +0700 Subject: [PATCH 3/8] =?UTF-8?q?feat:=20formatResetAbs=20=E2=80=94=20absolu?= =?UTF-8?q?te=20local=20reset=20wall-clock=20(HH:MM=20/=20Mon=20DD=20HH:MM?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/format.ts | 18 ++++++++++++++++++ test/lines.test.ts | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/src/format.ts b/src/format.ts index 90b0c47..353301b 100644 --- a/src/format.ts +++ b/src/format.ts @@ -42,3 +42,21 @@ export function formatReset(targetMs: number, now: number): string { } return `${hours}h ${minutes}m`; } + +// v0.3.x absolute reset wall-clock (reset-time feature): same-day resets read as +// clock time (`23:30`); anything further out carries an EN month-day (`Sep 12 +// 04:09`) — no weekday abbreviation (ambiguous when the reset lands on the +// viewer's own weekday). Machine-local like infoLine's clock; hardcoded EN months +// keep rendering deterministic (no Intl comma/platform variance). +const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const; + +export function formatResetAbs(targetMs: number, now: number): string { + if (targetMs <= now) return "now"; + const t = new Date(targetMs); + const n = new Date(now); + const sameDay = + t.getFullYear() === n.getFullYear() && + t.getMonth() === n.getMonth() && + t.getDate() === n.getDate(); + return sameDay ? formatClock(targetMs) : `${MONTHS[t.getMonth()]} ${t.getDate()} ${formatClock(targetMs)}`; +} diff --git a/test/lines.test.ts b/test/lines.test.ts index 85d81bf..ee89ba7 100644 --- a/test/lines.test.ts +++ b/test/lines.test.ts @@ -7,6 +7,7 @@ import type { SlTheme } from "../src/index.ts"; import type { DeenSnapshot } from "../src/deen/source.ts"; import type { PrayerScheduleEntry } from "../src/deen/time.ts"; import type { QuotaResult } from "../src/quota/zai.ts"; +import { formatResetAbs } from "../src/format.ts"; const theme: SlTheme = { fg: (token, text) => `<${token}>${text}` }; const sep = theme.fg("dim", " · "); @@ -65,6 +66,13 @@ test("zaiLine v0.3.0: percents keep heat; paren = pace · reset; lowercase 5hrs assert.equal(zaiLine(theme, none, now, sep), " 󰚯 zai — no quota windows"); }); +test("formatResetAbs: same-day clock, cross-day month-day, past → now", () => { + const now = new Date(2026, 8, 9, 10, 0).getTime(); // Wed 09 Sep 2026 10:00 local — DST-edge-free + assert.equal(formatResetAbs(now + HOUR, now), "11:00"); + assert.equal(formatResetAbs(now + 20 * HOUR, now), "Sep 10 06:00"); + assert.equal(formatResetAbs(now - 60_000, now), "now"); +}); + test("paceText: gap = window × Δ%/100; formats and over/under tokens", () => { const H = 3_600_000; assert.deepEqual(paceText(5 * H, 34, 74), { text: "2h 0m under", token: "success" }); // RECTOR's worked example From 08dc62e22b2e64ca868295ff0d949dff41a9e92b Mon Sep 17 00:00:00 2001 From: RECTOR Date: Wed, 9 Sep 2026 19:55:50 +0700 Subject: [PATCH 4/8] feat: zai windows render absolute reset time after the countdown --- src/index.ts | 3 ++- test/lines.test.ts | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 9f4540d..108a131 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ import { readFileSync, mkdirSync } from "node:fs"; import { fetchQuota, readZaiKey, type QuotaLimit, type QuotaResult } from "./quota/zai.ts"; import { FIVE_HOUR_MS, WEEK_MS, windowElapsedPercent } from "./quota/project.ts"; -import { formatReset } from "./format.ts"; +import { formatReset, formatResetAbs } from "./format.ts"; import { createDeenSource, type DeenSnapshot, type DeenSourceConfig } from "./deen/source.ts"; import { zaiStatusDetail } from "./adapters/zai.ts"; import { scanMoney, type MoneySnapshot } from "./money.ts"; @@ -178,6 +178,7 @@ function windowSeg(theme: SlTheme, label: string, lim: QuotaLimit, lengthMs: num theme.fg(pace.token, pace.text), theme.fg("dim", " · "), theme.fg("dim", formatReset(lim.nextResetTime, now)), + theme.fg("dim", ` · ${formatResetAbs(lim.nextResetTime, now)}`), theme.fg("dim", ")"), ].join(""); } diff --git a/test/lines.test.ts b/test/lines.test.ts index ee89ba7..d94c677 100644 --- a/test/lines.test.ts +++ b/test/lines.test.ts @@ -48,13 +48,13 @@ test("prayerLine: adhan-window prayer renders like past (dim ✓)", () => { assert.ok(prayerLine(theme, deen(schedule), sep).includes("Dhuhr 11:51 ✓")); }); -test("zaiLine v0.3.0: percents keep heat; paren = pace · reset; lowercase 5hrs label", () => { - const now = Date.now(); +test("zaiLine: percents keep heat; paren = pace · countdown · absolute; lowercase 5hrs label", () => { + const now = new Date(2026, 8, 9, 10, 0).getTime(); // Wed 09 Sep 2026 10:00 local — DST-edge-free // 5h window, nextReset 1h out → 80% elapsed. Usage 16% vs 80% → pace 5h×(16−80)/100 = 3h 12m under. const fiveHour = (percentage: number) => ({ unit: 1, number: 1, usage: 28000, currentValue: 7664, remaining: 20335, percentage, nextResetTime: now + HOUR }); const data = (p5: number): QuotaResult => ({ tier: "pro", fiveHour: fiveHour(p5), weekly: null, fetchedAt: now }); assert.ok(zaiLine(theme, data(16), now, sep).includes("5hrs 16%/80%")); - assert.ok(zaiLine(theme, data(16), now, sep).includes(" (3h 12m under · 1h 0m)")); + assert.ok(zaiLine(theme, data(16), now, sep).includes(" (3h 12m under · 1h 0m · 11:00)")); // over pace (usage > elapsed): 85% vs 80% → 5h×5/100 = 15m over, warning token assert.ok(zaiLine(theme, data(85), now, sep).includes("15m over")); assert.ok(zaiLine(theme, data(76), now, sep).includes("5hrs 76%/80%")); @@ -62,6 +62,7 @@ test("zaiLine v0.3.0: percents keep heat; paren = pace · reset; lowercase 5hrs assert.ok(zaiLine(theme, data(105), now, sep).includes("5hrs 100%+/80%")); const both: QuotaResult = { tier: "pro", fiveHour: fiveHour(16), weekly: { ...fiveHour(24), nextResetTime: now + 3 * DAY + 18 * HOUR }, fetchedAt: now }; assert.ok(zaiLine(theme, both, now, sep).includes(" · ")); + assert.ok(zaiLine(theme, both, now, sep).includes(" · Sep 13 04:00")); // cross-day → month-day form const none: QuotaResult = { tier: "pro", fiveHour: null, weekly: null, fetchedAt: now }; assert.equal(zaiLine(theme, none, now, sep), " 󰚯 zai — no quota windows"); }); From 80ac20325f715d5edbd16a64a97c76fdfdfddce4 Mon Sep 17 00:00:00 2001 From: RECTOR Date: Wed, 9 Sep 2026 20:02:31 +0700 Subject: [PATCH 5/8] docs: sync header/README/hero with absolute reset times; widen hero canvas to 980 --- README.md | 2 +- assets/hero.svg | 14 +++++++------- src/index.ts | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6ffddd3..7459196 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Render order (top → bottom): **info · prayers · money · zai**. | **info** | Local clock · Hijri date · city — all dim. | | **prayers** | All five prayers with wall times, from [aladhan](https://api.aladhan.com) (cached per local day, stale-marker on degradation). Past prayers get a dim `✓`; the next prayer is green with a countdown on its segment only — `Dhuhr 11:51 (3h 36m)`. | | **money** | API spend + token volume per window: `REPO $68.36 (1.3B)` (all-time for the current project) · `DAY` (since 00:00 local) · `7DAY` / `30DAY` (rolling, hour-aligned). Token volumes are dim, from the same sessions scan. | -| **zai** | Quota pace per window: `LABEL usage%/elapsed% (pace · reset)` — e.g. `5hrs 16%/80% (1h 36m over · 3h 43m)`. **Provider-gated**: renders only while the active model's provider is `zai` (or the provider is unreadable); vanishes entirely on other providers. | +| **zai** | Quota pace per window: `LABEL usage%/elapsed% (pace · reset · absolute)` — e.g. `5hrs 16%/80% (1h 36m over · 3h 43m · 11:58)`. **Provider-gated**: renders only while the active model's provider is `zai` (or the provider is unreadable); vanishes entirely on other providers. | **Pace** — `window × (usage% − elapsed%)/100` — renders `1h 36m over` (orange: burning faster than the clock, quota exhausts early by that much) or `1d 5h under` (green: behind the clock). Percent heat: accent < 70%, warning ≥ 70%, error ≥ 90% — raw, so over-quota stays error-red while display caps at `100%+`. diff --git a/assets/hero.svg b/assets/hero.svg index 49dae87..30e7a7f 100644 --- a/assets/hero.svg +++ b/assets/hero.svg @@ -1,4 +1,4 @@ - + omp-statusline widget rendering below the editor @@ -14,12 +14,12 @@ - - + + - omp — ~/local-dev/getpipher/omp-statusline + omp — ~/local-dev/getpipher/omp-statusline export default function ompStatusline(pi: SlApi): void { const cfg = loadLiveConfig(); const deen = createDeenSource({ cachePath: DEEN_CACHE, config: () => cfg.deen }); @@ -33,7 +33,7 @@ REPO $68.36 (1.3B) · DAY $26.50 (138.2M) · 7DAY $315.27 (2.7B) · 30DAY $492.88 (5.9B) - zai 5hrs 16%/80% (1h 36m over · 3h 43m) · 7DAY 24%/31% (1d 5h under · 4d 19h) - - omp native statusline closes the block — placement tracked upstream (oh-my-pi #11100) + zai 5hrs 16%/80% (1h 36m over · 3h 43m · 11:58) · 7DAY 24%/31% (1d 5h under · 4d 19h · Sep 12 03:15) + + omp native statusline closes the block — placement tracked upstream (oh-my-pi #11100) diff --git a/src/index.ts b/src/index.ts index 108a131..87ead5c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ // omp-statusline — 4-line belowEditor widget, RECTOR-approved 2026-09-07: -// 󰚯 zai 5HRS 16%/25% (3h 43m) · 7DAY 24%/31% (4d 19h) ← provider-gated (zai only) +// 󰚯 zai 5hrs 16%/80% (1h 36m over · 3h 43m · 11:58) · 7DAY 24%/31% (1d 5h under · 4d 19h · Sep 12 03:15) ← provider-gated (zai only) // 󰣎 Fajr 04:33 ✓ · Dhuhr 11:51 (3h 36m) · Asr 15:07 · Maghrib 17:52 · Isha 19:01 // 󰥔 08:15 · 25 Rabīʿ al-awwal 1448 · Jakarta // 󰄬 REPO $68.36 · DAY $26.50 · 7DAY $315.27 · 30DAY $492.88 From 403b52c57f7dca47b72d64bd0f30e0f724aab0cb Mon Sep 17 00:00:00 2001 From: RECTOR Date: Wed, 9 Sep 2026 20:11:43 +0700 Subject: [PATCH 6/8] chore: bump version to 0.5.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index aaae0c9..7167586 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@getpipher/omp-statusline", - "version": "0.4.0", + "version": "0.5.0", "description": "Four-line omp statusline widget: zai coding-plan quota (provider-gated), prayer times, hijri clock, and API-spend ledger (REPO/DAY/7DAY/30DAY, subagent-inclusive).", "keywords": [ "pi-package", From 46a73998d49ef527c023cd972481faf010cf619a Mon Sep 17 00:00:00 2001 From: RECTOR Date: Wed, 9 Sep 2026 20:27:09 +0700 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20review=20findings=20=E2=80=94=20cohe?= =?UTF-8?q?rent=20doc=20mocks,=20padded=20single-digit=20day,=20midnight?= =?UTF-8?q?=20tests,=20version=20citation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 +- assets/hero.svg | 2 +- src/format.ts | 4 ++-- src/index.ts | 2 +- test/lines.test.ts | 3 +++ 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7459196..fa38f06 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Render order (top → bottom): **info · prayers · money · zai**. | **info** | Local clock · Hijri date · city — all dim. | | **prayers** | All five prayers with wall times, from [aladhan](https://api.aladhan.com) (cached per local day, stale-marker on degradation). Past prayers get a dim `✓`; the next prayer is green with a countdown on its segment only — `Dhuhr 11:51 (3h 36m)`. | | **money** | API spend + token volume per window: `REPO $68.36 (1.3B)` (all-time for the current project) · `DAY` (since 00:00 local) · `7DAY` / `30DAY` (rolling, hour-aligned). Token volumes are dim, from the same sessions scan. | -| **zai** | Quota pace per window: `LABEL usage%/elapsed% (pace · reset · absolute)` — e.g. `5hrs 16%/80% (1h 36m over · 3h 43m · 11:58)`. **Provider-gated**: renders only while the active model's provider is `zai` (or the provider is unreadable); vanishes entirely on other providers. | +| **zai** | Quota pace per window: `LABEL usage%/elapsed% (pace · reset · absolute)` — e.g. `5hrs 16%/26% (30m under · 3h 43m · 11:58)`. **Provider-gated**: renders only while the active model's provider is `zai` (or the provider is unreadable); vanishes entirely on other providers. | **Pace** — `window × (usage% − elapsed%)/100` — renders `1h 36m over` (orange: burning faster than the clock, quota exhausts early by that much) or `1d 5h under` (green: behind the clock). Percent heat: accent < 70%, warning ≥ 70%, error ≥ 90% — raw, so over-quota stays error-red while display caps at `100%+`. diff --git a/assets/hero.svg b/assets/hero.svg index 30e7a7f..242ad03 100644 --- a/assets/hero.svg +++ b/assets/hero.svg @@ -33,7 +33,7 @@ REPO $68.36 (1.3B) · DAY $26.50 (138.2M) · 7DAY $315.27 (2.7B) · 30DAY $492.88 (5.9B) - zai 5hrs 16%/80% (1h 36m over · 3h 43m · 11:58) · 7DAY 24%/31% (1d 5h under · 4d 19h · Sep 12 03:15) + zai 5hrs 16%/26% (30m under · 3h 43m · 11:58) · 7DAY 33%/31% (3h 22m over · 4d 19h · Sep 12 03:15) omp native statusline closes the block — placement tracked upstream (oh-my-pi #11100) diff --git a/src/format.ts b/src/format.ts index 353301b..236c564 100644 --- a/src/format.ts +++ b/src/format.ts @@ -43,7 +43,7 @@ export function formatReset(targetMs: number, now: number): string { return `${hours}h ${minutes}m`; } -// v0.3.x absolute reset wall-clock (reset-time feature): same-day resets read as +// v0.5.0 absolute reset wall-clock (reset-time feature): same-day resets read as // clock time (`23:30`); anything further out carries an EN month-day (`Sep 12 // 04:09`) — no weekday abbreviation (ambiguous when the reset lands on the // viewer's own weekday). Machine-local like infoLine's clock; hardcoded EN months @@ -58,5 +58,5 @@ export function formatResetAbs(targetMs: number, now: number): string { t.getFullYear() === n.getFullYear() && t.getMonth() === n.getMonth() && t.getDate() === n.getDate(); - return sameDay ? formatClock(targetMs) : `${MONTHS[t.getMonth()]} ${t.getDate()} ${formatClock(targetMs)}`; + return sameDay ? formatClock(targetMs) : `${MONTHS[t.getMonth()]} ${String(t.getDate()).padStart(2, "0")} ${formatClock(targetMs)}`; } diff --git a/src/index.ts b/src/index.ts index 87ead5c..bad849d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ // omp-statusline — 4-line belowEditor widget, RECTOR-approved 2026-09-07: -// 󰚯 zai 5hrs 16%/80% (1h 36m over · 3h 43m · 11:58) · 7DAY 24%/31% (1d 5h under · 4d 19h · Sep 12 03:15) ← provider-gated (zai only) +// 󰚯 zai 5hrs 16%/26% (30m under · 3h 43m · 11:58) · 7DAY 33%/31% (3h 22m over · 4d 19h · Sep 12 03:15) ← provider-gated (zai only) // 󰣎 Fajr 04:33 ✓ · Dhuhr 11:51 (3h 36m) · Asr 15:07 · Maghrib 17:52 · Isha 19:01 // 󰥔 08:15 · 25 Rabīʿ al-awwal 1448 · Jakarta // 󰄬 REPO $68.36 · DAY $26.50 · 7DAY $315.27 · 30DAY $492.88 diff --git a/test/lines.test.ts b/test/lines.test.ts index d94c677..a084207 100644 --- a/test/lines.test.ts +++ b/test/lines.test.ts @@ -72,6 +72,9 @@ test("formatResetAbs: same-day clock, cross-day month-day, past → now", () => assert.equal(formatResetAbs(now + HOUR, now), "11:00"); assert.equal(formatResetAbs(now + 20 * HOUR, now), "Sep 10 06:00"); assert.equal(formatResetAbs(now - 60_000, now), "now"); + const lateNight = new Date(2026, 8, 4, 23, 0).getTime(); // Fri 04 Sep 23:00 local + assert.equal(formatResetAbs(lateNight + 59 * 60_000, lateNight), "23:59"); // same-day branch near midnight + assert.equal(formatResetAbs(lateNight + 2 * HOUR, lateNight), "Sep 05 01:00"); // crosses midnight → padded single-digit day }); test("paceText: gap = window × Δ%/100; formats and over/under tokens", () => { From a5a34687b2635b9811650e56f8c539e68d4f4d8e Mon Sep 17 00:00:00 2001 From: RECTOR Date: Wed, 9 Sep 2026 20:34:54 +0700 Subject: [PATCH 8/8] fix: 7DAY doc mock reset 03:15 -> 03:30 (rounding-exact at the mock instant) --- assets/hero.svg | 2 +- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/hero.svg b/assets/hero.svg index 242ad03..976f026 100644 --- a/assets/hero.svg +++ b/assets/hero.svg @@ -33,7 +33,7 @@ REPO $68.36 (1.3B) · DAY $26.50 (138.2M) · 7DAY $315.27 (2.7B) · 30DAY $492.88 (5.9B) - zai 5hrs 16%/26% (30m under · 3h 43m · 11:58) · 7DAY 33%/31% (3h 22m over · 4d 19h · Sep 12 03:15) + zai 5hrs 16%/26% (30m under · 3h 43m · 11:58) · 7DAY 33%/31% (3h 22m over · 4d 19h · Sep 12 03:30) omp native statusline closes the block — placement tracked upstream (oh-my-pi #11100) diff --git a/src/index.ts b/src/index.ts index bad849d..4437a5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ // omp-statusline — 4-line belowEditor widget, RECTOR-approved 2026-09-07: -// 󰚯 zai 5hrs 16%/26% (30m under · 3h 43m · 11:58) · 7DAY 33%/31% (3h 22m over · 4d 19h · Sep 12 03:15) ← provider-gated (zai only) +// 󰚯 zai 5hrs 16%/26% (30m under · 3h 43m · 11:58) · 7DAY 33%/31% (3h 22m over · 4d 19h · Sep 12 03:30) ← provider-gated (zai only) // 󰣎 Fajr 04:33 ✓ · Dhuhr 11:51 (3h 36m) · Asr 15:07 · Maghrib 17:52 · Isha 19:01 // 󰥔 08:15 · 25 Rabīʿ al-awwal 1448 · Jakarta // 󰄬 REPO $68.36 · DAY $26.50 · 7DAY $315.27 · 30DAY $492.88