pi-deepseek-tariff 0.3.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -7
- package/extensions/deepseek-peak.ts +32 -10
- package/package.json +1 -1
- package/src/rules.ts +43 -1
- package/src/view.ts +41 -12
package/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Commands: /dsp on|off|refresh|en|ru · /dsp footer on|off
|
|
|
38
38
|
pi install npm:pi-deepseek-tariff
|
|
39
39
|
|
|
40
40
|
# from git: tags mirror the npm versions, so pin whichever you want
|
|
41
|
-
pi install git:github.com/valderan/pi-deepseek-tariff@v0.
|
|
41
|
+
pi install git:github.com/valderan/pi-deepseek-tariff@v0.4.0
|
|
42
42
|
|
|
43
43
|
# local checkout, for development
|
|
44
44
|
git clone git@github.com:valderan/pi-deepseek-tariff.git
|
|
@@ -70,6 +70,18 @@ Type `/dsp ` and press Tab: the arguments above are offered as completions, with
|
|
|
70
70
|
|
|
71
71
|
The tariff is spelled `PEAK` / `OFF-PEAK` in English regardless of the panel language; only the countdown units are localized, so Russian reads `◷ deepseek OFF-PEAK → 11ч 59м`.
|
|
72
72
|
|
|
73
|
+
The line escalates as the switch approaches — clock and countdown first, then the tariff word itself:
|
|
74
|
+
|
|
75
|
+
| Left to the switch | Clock + countdown | Tariff word |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| more than 15 min | state colour / accent | whole word in the state colour |
|
|
78
|
+
| below 15 min | **yellow** (`warning`) | unchanged |
|
|
79
|
+
| counter shows `4m` / `3m` / `2m` / `1m` / `0m` | yellow | last **1 / 2 / 3 / 3 / 4** letters take the incoming colour, the rest dims |
|
|
80
|
+
|
|
81
|
+
So the word is eaten from the end, one letter per minute of the visible countdown: `OFF-PEA` + `K`, then `OFF-PE` + `AK`, … up to `OFF-` + `PEAK` in the final seconds. The tail is **red** when PEAK is about to start and **green** when off-peak is about to start, and the tick drops to 1 s during the last five minutes so each step lands on the minute.
|
|
82
|
+
|
|
83
|
+

|
|
84
|
+
|
|
73
85
|
Pi renders every extension status on a single footer line, joined alphabetically, so the segment is kept short.
|
|
74
86
|
- **Peak rules** — DeepSeek defines peak as `01:00-04:00` and `06:00-10:00` **UTC, Monday to Friday**; weekends and Chinese public holidays are off-peak all day.
|
|
75
87
|
- **Local time everywhere** — the UTC clock comes from the machine clock, so peak windows, weekday labels and countdowns are rendered in *your* zone (`(MSK)`, `(UTC-04:00)`, …). Weekday labels shift with the zone: at UTC−4 the windows land on Sun–Thu.
|
|
@@ -96,7 +108,7 @@ Type `/dsp ` and press Tab: the arguments above are offered as completions, with
|
|
|
96
108
|
| Key | Default | Meaning |
|
|
97
109
|
|---|---|---|
|
|
98
110
|
| `language` | pi's `settings.json` language, else `en` | `"en"` or `"ru"` |
|
|
99
|
-
| `intervalMs` | `30000` |
|
|
111
|
+
| `intervalMs` | `30000` | repaint cadence (clamped to ≥ 5000); the footer switches to a 1 s tick by itself in the last five minutes before a switch |
|
|
100
112
|
| `pricingTtlHours` | `6` | how long parsed prices stay valid |
|
|
101
113
|
| `alwaysShow` | `false` | show the footer line for non-DeepSeek models too |
|
|
102
114
|
| `footer` | `true` | show the footer status line at all; `/dsp footer off` writes this |
|
|
@@ -136,11 +148,11 @@ the commit whose `package.json` says `X.Y.Z`, and npm publishes that same versio
|
|
|
136
148
|
|
|
137
149
|
```bash
|
|
138
150
|
# 1. bump package.json, add a CHANGELOG entry, commit
|
|
139
|
-
npm version 0.
|
|
140
|
-
git commit -am "chore: release 0.
|
|
151
|
+
npm version 0.4.0 --no-git-tag-version # or edit by hand
|
|
152
|
+
git commit -am "chore: release 0.4.0"
|
|
141
153
|
|
|
142
154
|
# 2. tag the release commit and push
|
|
143
|
-
git tag -a v0.
|
|
155
|
+
git tag -a v0.4.0 -m v0.4.0
|
|
144
156
|
git push origin main --tags
|
|
145
157
|
|
|
146
158
|
# 3. publish the tag to npm (2FA prompt, or --otp=123456)
|
|
@@ -151,7 +163,7 @@ npm publish --access public
|
|
|
151
163
|
Check the result:
|
|
152
164
|
|
|
153
165
|
```bash
|
|
154
|
-
npm view pi-deepseek-tariff dist-tags # { latest: '0.
|
|
166
|
+
npm view pi-deepseek-tariff dist-tags # { latest: '0.4.0' }
|
|
155
167
|
npm view pi-deepseek-tariff versions
|
|
156
168
|
```
|
|
157
169
|
|
|
@@ -159,7 +171,7 @@ npm view pi-deepseek-tariff versions
|
|
|
159
171
|
`NPM_TOKEN` secret is configured; with trusted publishing enabled on npmjs.com the
|
|
160
172
|
token is not needed at all and the build is signed with provenance.
|
|
161
173
|
|
|
162
|
-
Tags are pinnable with `pi install git:…@v0.
|
|
174
|
+
Tags are pinnable with `pi install git:…@v0.4.0`. Published versions show up in the
|
|
163
175
|
[pi package gallery](https://pi.dev/packages) through the `pi-package` keyword.
|
|
164
176
|
|
|
165
177
|
## Changelog
|
|
@@ -14,15 +14,22 @@ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } f
|
|
|
14
14
|
import { loadConfig, PRICING_CACHE, saveConfig, type Config } from "../src/config.ts";
|
|
15
15
|
import { STRINGS, type Lang } from "../src/i18n.ts";
|
|
16
16
|
import { createPricingStore, isOffline, type Pricing } from "../src/pricing.ts";
|
|
17
|
+
import { nextTransition } from "../src/rules.ts";
|
|
17
18
|
import { SEPARATOR, panelLines, statusText } from "../src/view.ts";
|
|
18
19
|
|
|
19
20
|
const STATUS_KEY = "deepseek-peak";
|
|
20
21
|
const WIDGET_KEY = "deepseek-peak";
|
|
21
22
|
|
|
23
|
+
/** Tick used while the footer counts down the last minutes before a switch. */
|
|
24
|
+
const FAST_TICK_MS = 1_000;
|
|
25
|
+
|
|
26
|
+
/** The footer changes every minute below this gap, so it needs the fast tick. */
|
|
27
|
+
const FAST_WINDOW_MS = 5 * 60_000;
|
|
28
|
+
|
|
22
29
|
export default function deepseekPeak(pi: ExtensionAPI) {
|
|
23
30
|
let config = loadConfig();
|
|
24
31
|
let lang: Lang = config.language ?? "en";
|
|
25
|
-
let timer: ReturnType<typeof
|
|
32
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
26
33
|
let panelVisible = false;
|
|
27
34
|
|
|
28
35
|
const pricing = createPricingStore({
|
|
@@ -80,21 +87,36 @@ export default function deepseekPeak(pi: ExtensionAPI) {
|
|
|
80
87
|
};
|
|
81
88
|
|
|
82
89
|
const stop = () => {
|
|
83
|
-
if (timer !== undefined)
|
|
90
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
84
91
|
timer = undefined;
|
|
85
92
|
};
|
|
86
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Normally the footer only needs `intervalMs`, but in the last five minutes the
|
|
96
|
+
* countdown word changes every minute, so poll faster to land on the boundary.
|
|
97
|
+
*/
|
|
98
|
+
const nextDelay = () => {
|
|
99
|
+
const msLeft = nextTransition(new Date(), config.holidays).getTime() - Date.now();
|
|
100
|
+
return msLeft < FAST_WINDOW_MS ? Math.min(FAST_TICK_MS, config.intervalMs) : config.intervalMs;
|
|
101
|
+
};
|
|
102
|
+
|
|
87
103
|
const start = (ctx: ExtensionContext) => {
|
|
88
104
|
stop();
|
|
89
105
|
render(ctx);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
106
|
+
|
|
107
|
+
// A self-rescheduling timeout instead of an interval: the cadence follows the countdown.
|
|
108
|
+
const schedule = () => {
|
|
109
|
+
timer = setTimeout(() => {
|
|
110
|
+
const paint = (value?: Pricing) => {
|
|
111
|
+
render(ctx, value);
|
|
112
|
+
schedule();
|
|
113
|
+
};
|
|
114
|
+
// Only hits the network when the TTL expired.
|
|
115
|
+
if (pricing.expired()) void pricing.refresh().then(paint);
|
|
116
|
+
else paint();
|
|
117
|
+
}, nextDelay());
|
|
118
|
+
};
|
|
119
|
+
schedule();
|
|
98
120
|
};
|
|
99
121
|
|
|
100
122
|
pi.on("session_start", async (_event, ctx) => {
|
package/package.json
CHANGED
package/src/rules.ts
CHANGED
|
@@ -111,8 +111,12 @@ export function clockTime(date: Date, lang: Lang): string {
|
|
|
111
111
|
return date.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" });
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
export function countdownMinutes(ms: number): number {
|
|
115
|
+
return Math.max(0, Math.round(ms / 60_000));
|
|
116
|
+
}
|
|
117
|
+
|
|
114
118
|
export function humanize(ms: number, lang: Lang = "en"): string {
|
|
115
|
-
const minutes =
|
|
119
|
+
const minutes = countdownMinutes(ms);
|
|
116
120
|
const hourUnit = lang === "ru" ? "ч" : "h";
|
|
117
121
|
const minuteUnit = lang === "ru" ? "м" : "m";
|
|
118
122
|
if (minutes < 60) return `${minutes}${minuteUnit}`;
|
|
@@ -121,6 +125,44 @@ export function humanize(ms: number, lang: Lang = "en"): string {
|
|
|
121
125
|
return rest === 0 ? `${hours}${hourUnit}` : `${hours}${hourUnit} ${rest}${minuteUnit}`;
|
|
122
126
|
}
|
|
123
127
|
|
|
128
|
+
export type CountdownPhase = {
|
|
129
|
+
/** Minutes the counter shows, so the animation can follow the visible number. */
|
|
130
|
+
minutes: number;
|
|
131
|
+
/** Less than 15 minutes to the switch: the clock and the countdown turn yellow. */
|
|
132
|
+
urgent: boolean;
|
|
133
|
+
/** Trailing letters of the tariff word painted in the incoming colour (0..4). */
|
|
134
|
+
swallowed: number;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/** Yellow warning starts below this gap to the next switch. */
|
|
138
|
+
export const URGENT_MS = 15 * 60_000;
|
|
139
|
+
|
|
140
|
+
/** A four-letter tail is all the animation ever covers, so `PEAK` is eaten whole. */
|
|
141
|
+
export const MAX_SWALLOWED = 4;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Letters of the four-letter tail already painted in the incoming colour, keyed by
|
|
145
|
+
* the minute the counter shows. `1` repeats the `2` step, and the whole tail goes
|
|
146
|
+
* only when the counter drops to `0` in the last seconds.
|
|
147
|
+
*/
|
|
148
|
+
const SWALLOWED_BY_MINUTES: Readonly<Record<number, number>> = { 0: 4, 1: 3, 2: 3, 3: 2, 4: 1 };
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* How close the next switch is, expressed as what the footer should paint.
|
|
152
|
+
*
|
|
153
|
+
* The step follows the *displayed* countdown (`4м` → one letter, `3м` → two, …),
|
|
154
|
+
* so the colours change exactly when the number changes instead of up to a
|
|
155
|
+
* minute earlier.
|
|
156
|
+
*/
|
|
157
|
+
export function countdownPhase(msLeft: number): CountdownPhase {
|
|
158
|
+
const minutes = countdownMinutes(msLeft);
|
|
159
|
+
return {
|
|
160
|
+
minutes,
|
|
161
|
+
urgent: msLeft < URGENT_MS,
|
|
162
|
+
swallowed: minutes >= 5 ? 0 : (SWALLOWED_BY_MINUTES[minutes] ?? 0),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
124
166
|
/** Local wall-clock time of a UTC hour on the machine's current date. */
|
|
125
167
|
export function localClock(date: Date, utcHour: number): string {
|
|
126
168
|
const instant = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), utcHour, 0, 0));
|
package/src/view.ts
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
-
import {
|
|
11
|
+
import type { Color } from "@earendil-works/pi-tui";
|
|
12
12
|
import type { Config } from "./config.ts";
|
|
13
13
|
import { localizeUnit, STRINGS, type Lang } from "./i18n.ts";
|
|
14
14
|
import type { Pricing, RequestPricing } from "./pricing.ts";
|
|
15
15
|
import {
|
|
16
|
+
countdownPhase,
|
|
16
17
|
dayRangeLabel,
|
|
17
18
|
humanize,
|
|
18
19
|
isHoliday,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
PEAK_WINDOWS_UTC,
|
|
25
26
|
tzLabel,
|
|
26
27
|
utcStamp,
|
|
28
|
+
type CountdownPhase,
|
|
27
29
|
} from "./rules.ts";
|
|
28
30
|
|
|
29
31
|
/** Placeholder for a full-width rule; the renderer expands it to the panel width. */
|
|
@@ -32,8 +34,12 @@ export const SEPARATOR = "\u0000rule";
|
|
|
32
34
|
/** Footer clock: one cell wide, so the segment stays short on the shared status line. */
|
|
33
35
|
const CLOCK = "\u25F7"; // ◷
|
|
34
36
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
+
/**
|
|
38
|
+
* DeepSeek brand blue (#4175E6). Fixed in every theme: it is the product colour, not a
|
|
39
|
+
* theme role. Written as a plain `Color` literal rather than `parseColor("#4175E6")`, so
|
|
40
|
+
* this module keeps zero runtime imports — the test job runs without `npm install`.
|
|
41
|
+
*/
|
|
42
|
+
const BRAND_BLUE: Color = { kind: "rgb", r: 0x41, g: 0x75, b: 0xe6 };
|
|
37
43
|
|
|
38
44
|
/** "Search & Fetch" value: a shared price is printed once. */
|
|
39
45
|
export function requestPricingText(entries: RequestPricing[], lang: Lang): string | undefined {
|
|
@@ -63,12 +69,31 @@ export function requestPricingText(entries: RequestPricing[], lang: Lang): strin
|
|
|
63
69
|
}
|
|
64
70
|
|
|
65
71
|
/**
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
72
|
+
* The tariff word with its tail painted as the switch approaches: the letters the
|
|
73
|
+
* feed already consumed go dim and the last up to four take the colour of the tariff
|
|
74
|
+
* that is about to start (red towards PEAK, green towards OFF-PEAK).
|
|
75
|
+
*/
|
|
76
|
+
function tariffWord(
|
|
77
|
+
theme: Theme,
|
|
78
|
+
word: string,
|
|
79
|
+
current: "success" | "error",
|
|
80
|
+
incoming: "success" | "error",
|
|
81
|
+
phase: CountdownPhase,
|
|
82
|
+
): string {
|
|
83
|
+
if (phase.swallowed <= 0) return theme.fg(current, word);
|
|
84
|
+
const cut = Math.max(0, word.length - phase.swallowed);
|
|
85
|
+
const head = cut > 0 ? theme.fg("dim", word.slice(0, cut)) : "";
|
|
86
|
+
return `${head}${theme.fg(incoming, word.slice(cut))}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Compact footer line: a clock, the brand in DeepSeek blue, the tariff word and
|
|
91
|
+
* the countdown — for example "◷ deepseek OFF-PEAK → 11h 59m".
|
|
69
92
|
*
|
|
70
|
-
* The clock carries the state colour
|
|
71
|
-
*
|
|
93
|
+
* The clock carries the state colour (green off-peak, red peak). Below 15 minutes
|
|
94
|
+
* to the next switch both the clock and the countdown turn yellow, and in the last
|
|
95
|
+
* four minutes the tariff word is eaten from the end, one letter per minute shown
|
|
96
|
+
* by the counter. The duration keeps the panel language units.
|
|
72
97
|
*/
|
|
73
98
|
export function statusText(theme: Theme, config: Config, lang: Lang, pricing: Pricing): string {
|
|
74
99
|
void pricing;
|
|
@@ -76,12 +101,16 @@ export function statusText(theme: Theme, config: Config, lang: Lang, pricing: Pr
|
|
|
76
101
|
const now = new Date();
|
|
77
102
|
const peak = isPeak(now, config.holidays);
|
|
78
103
|
const next = nextTransition(now, config.holidays);
|
|
104
|
+
const msLeft = next.getTime() - now.getTime();
|
|
105
|
+
const phase = countdownPhase(msLeft);
|
|
79
106
|
const state = peak ? "error" : "success";
|
|
80
|
-
const
|
|
81
|
-
const clock = theme.fg(state, CLOCK);
|
|
107
|
+
const incoming = peak ? "success" : "error";
|
|
108
|
+
const clock = theme.fg(phase.urgent ? "warning" : state, CLOCK);
|
|
82
109
|
const brand = theme.style(text.statusBrand, { fg: BRAND_BLUE });
|
|
83
|
-
const
|
|
84
|
-
|
|
110
|
+
const word = peak ? text.statusPeak : text.statusOffPeak;
|
|
111
|
+
const label = tariffWord(theme, word, state, incoming, phase);
|
|
112
|
+
const countdown = theme.fg(phase.urgent ? "warning" : "accent", text.statusFor(humanize(msLeft, lang)));
|
|
113
|
+
return `${clock} ${brand} ${label} ${countdown}`;
|
|
85
114
|
}
|
|
86
115
|
|
|
87
116
|
/**
|