pi-deepseek-tariff 0.3.2 → 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 +23 -11
- package/extensions/deepseek-peak.ts +32 -10
- package/package.json +1 -1
- package/src/i18n.ts +13 -8
- package/src/rules.ts +43 -1
- package/src/view.ts +66 -10
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A [pi](https://pi.dev) extension that shows the current **DeepSeek API tariff wi
|
|
|
10
10
|
DeepSeek charges double during peak hours, so knowing the current window is worth real money.
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
|
|
13
|
+
◷ deepseek OFF-PEAK → 11h 59m ← footer line, always visible
|
|
14
14
|
|
|
15
15
|
Now OFF-PEAK
|
|
16
16
|
Computer: 14:43:52 26-10-2026(MSK) | UTC: 2026-10-26 11:43
|
|
@@ -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
|
|
@@ -66,14 +66,26 @@ Type `/dsp ` and press Tab: the arguments above are offered as completions, with
|
|
|
66
66
|
|
|
67
67
|
## What it does
|
|
68
68
|
|
|
69
|
-
- **Footer line** — one compact status line:
|
|
69
|
+
- **Footer line** — one compact status line: `◷ deepseek OFF-PEAK → 11h 59m`, or `◷ deepseek PEAK → 1h 40m` during peak. The clock glyph leads the line and takes the state colour — green for off-peak, red for peak — and the tariff word next to it is coloured the same way. The brand is written lowercase in DeepSeek blue (`#4175E6`), the countdown uses the theme accent. Shown only while a DeepSeek model is active; `/dsp footer off` hides it for good.
|
|
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
|
+
|
|
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
|
+

|
|
72
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.
|
|
76
|
-
- **Prices** — parsed from `api-docs.deepseek.com/quick_start/pricing/`: cache hit, cache miss and output per model, plus the Search & Fetch request rates. The tariff in effect is highlighted.
|
|
88
|
+
- **Prices** — parsed from `api-docs.deepseek.com/quick_start/pricing/`: cache hit, cache miss and output per model, plus the Search & Fetch request rates. The tariff in effect is highlighted, and the `DeepSeek` word in the `DeepSeek prices:` label keeps the same brand blue as the footer.
|
|
77
89
|
- **Caching** — the page is parsed at most once per `pricingTtlHours` (default 6), and the result is cached on disk, so restarts stay offline. `PI_OFFLINE` disables network access entirely and the panel falls back to built-in values.
|
|
78
90
|
|
|
79
91
|
## Configuration
|
|
@@ -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/i18n.ts
CHANGED
|
@@ -23,6 +23,8 @@ type Strings = {
|
|
|
23
23
|
nextIn: (duration: string) => string;
|
|
24
24
|
and: string;
|
|
25
25
|
prices: string;
|
|
26
|
+
/** The product name inside `prices`, rendered in the DeepSeek blue instead of the label token. */
|
|
27
|
+
pricesBrand: string;
|
|
26
28
|
pricesState: (state: string) => string;
|
|
27
29
|
activeModel: string;
|
|
28
30
|
peakWindows: (zone: string) => string;
|
|
@@ -34,6 +36,7 @@ type Strings = {
|
|
|
34
36
|
sourceBuiltin: string;
|
|
35
37
|
requestPricing: string;
|
|
36
38
|
commands: string;
|
|
39
|
+
/** Footer line — the state is always written in English (PEAK / OFF-PEAK), in every language. */
|
|
37
40
|
statusBrand: string;
|
|
38
41
|
statusPeak: string;
|
|
39
42
|
statusOffPeak: string;
|
|
@@ -53,6 +56,7 @@ export const STRINGS: Record<Lang, Strings> = {
|
|
|
53
56
|
nextIn: (duration) => `in ${duration}`,
|
|
54
57
|
and: "and",
|
|
55
58
|
prices: "DeepSeek prices:",
|
|
59
|
+
pricesBrand: "DeepSeek",
|
|
56
60
|
pricesState: (state) => `$ per 1M tokens, active: ${state}`,
|
|
57
61
|
activeModel: "← active",
|
|
58
62
|
peakWindows: (zone) => `Peak windows (${zone}):`,
|
|
@@ -64,10 +68,10 @@ export const STRINGS: Record<Lang, Strings> = {
|
|
|
64
68
|
sourceBuiltin: "built-in values (page unavailable)",
|
|
65
69
|
requestPricing: "Search & Fetch:",
|
|
66
70
|
commands: "Commands:",
|
|
67
|
-
statusBrand: "
|
|
68
|
-
statusPeak: "
|
|
69
|
-
statusOffPeak: "
|
|
70
|
-
statusFor: (duration) =>
|
|
71
|
+
statusBrand: "deepseek",
|
|
72
|
+
statusPeak: "PEAK",
|
|
73
|
+
statusOffPeak: "OFF-PEAK",
|
|
74
|
+
statusFor: (duration) => `\u2192 ${duration}`,
|
|
71
75
|
locale: "en-GB",
|
|
72
76
|
hourUnit: "h",
|
|
73
77
|
minuteUnit: "m",
|
|
@@ -81,6 +85,7 @@ export const STRINGS: Record<Lang, Strings> = {
|
|
|
81
85
|
nextIn: (duration) => `через ${duration}`,
|
|
82
86
|
and: "и",
|
|
83
87
|
prices: "Цены DeepSeek:",
|
|
88
|
+
pricesBrand: "DeepSeek",
|
|
84
89
|
pricesState: (state) => `$ за 1M токенов, активен ${state}`,
|
|
85
90
|
activeModel: "← активна",
|
|
86
91
|
peakWindows: (zone) => `Окна peak (${zone}):`,
|
|
@@ -92,10 +97,10 @@ export const STRINGS: Record<Lang, Strings> = {
|
|
|
92
97
|
sourceBuiltin: "встроенные значения (страница недоступна)",
|
|
93
98
|
requestPricing: "Search & Fetch:",
|
|
94
99
|
commands: "Команды:",
|
|
95
|
-
statusBrand: "
|
|
96
|
-
statusPeak: "
|
|
97
|
-
statusOffPeak: "
|
|
98
|
-
statusFor: (duration) =>
|
|
100
|
+
statusBrand: "deepseek",
|
|
101
|
+
statusPeak: "PEAK",
|
|
102
|
+
statusOffPeak: "OFF-PEAK",
|
|
103
|
+
statusFor: (duration) => `\u2192 ${duration}`,
|
|
99
104
|
locale: "ru-RU",
|
|
100
105
|
hourUnit: "ч",
|
|
101
106
|
minuteUnit: "м",
|
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
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Presentation: the footer status line and the panel body.
|
|
3
3
|
*
|
|
4
|
-
* Colors follow the pi theme:
|
|
5
|
-
* token,
|
|
4
|
+
* Colors follow the pi theme: the footer clock and the tariff word share the
|
|
5
|
+
* state token (green off-peak, red peak), the countdown uses the accent token,
|
|
6
|
+
* and only the brand keeps a fixed colour. The panel below the editor keeps the
|
|
7
|
+
* warning/text pairing.
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { Color } from "@earendil-works/pi-tui";
|
|
9
12
|
import type { Config } from "./config.ts";
|
|
10
13
|
import { localizeUnit, STRINGS, type Lang } from "./i18n.ts";
|
|
11
14
|
import type { Pricing, RequestPricing } from "./pricing.ts";
|
|
12
15
|
import {
|
|
16
|
+
countdownPhase,
|
|
13
17
|
dayRangeLabel,
|
|
14
18
|
humanize,
|
|
15
19
|
isHoliday,
|
|
@@ -21,11 +25,22 @@ import {
|
|
|
21
25
|
PEAK_WINDOWS_UTC,
|
|
22
26
|
tzLabel,
|
|
23
27
|
utcStamp,
|
|
28
|
+
type CountdownPhase,
|
|
24
29
|
} from "./rules.ts";
|
|
25
30
|
|
|
26
31
|
/** Placeholder for a full-width rule; the renderer expands it to the panel width. */
|
|
27
32
|
export const SEPARATOR = "\u0000rule";
|
|
28
33
|
|
|
34
|
+
/** Footer clock: one cell wide, so the segment stays short on the shared status line. */
|
|
35
|
+
const CLOCK = "\u25F7"; // ◷
|
|
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 };
|
|
43
|
+
|
|
29
44
|
/** "Search & Fetch" value: a shared price is printed once. */
|
|
30
45
|
export function requestPricingText(entries: RequestPricing[], lang: Lang): string | undefined {
|
|
31
46
|
if (entries.length === 0) return undefined;
|
|
@@ -54,9 +69,31 @@ export function requestPricingText(entries: RequestPricing[], lang: Lang): strin
|
|
|
54
69
|
}
|
|
55
70
|
|
|
56
71
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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".
|
|
92
|
+
*
|
|
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.
|
|
60
97
|
*/
|
|
61
98
|
export function statusText(theme: Theme, config: Config, lang: Lang, pricing: Pricing): string {
|
|
62
99
|
void pricing;
|
|
@@ -64,10 +101,29 @@ export function statusText(theme: Theme, config: Config, lang: Lang, pricing: Pr
|
|
|
64
101
|
const now = new Date();
|
|
65
102
|
const peak = isPeak(now, config.holidays);
|
|
66
103
|
const next = nextTransition(now, config.holidays);
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
|
|
104
|
+
const msLeft = next.getTime() - now.getTime();
|
|
105
|
+
const phase = countdownPhase(msLeft);
|
|
106
|
+
const state = peak ? "error" : "success";
|
|
107
|
+
const incoming = peak ? "success" : "error";
|
|
108
|
+
const clock = theme.fg(phase.urgent ? "warning" : state, CLOCK);
|
|
109
|
+
const brand = theme.style(text.statusBrand, { fg: BRAND_BLUE });
|
|
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}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Colour the product name inside a panel label without splitting the string in
|
|
118
|
+
* i18n: the label stays one translatable line, only the brand word is swapped to
|
|
119
|
+
* the DeepSeek blue. Falls back to `rest(label)` when the name is missing.
|
|
120
|
+
*/
|
|
121
|
+
function brandLabel(theme: Theme, label: string, brand: string, rest: (value: string) => string): string {
|
|
122
|
+
const at = label.indexOf(brand);
|
|
123
|
+
if (at < 0) return rest(label);
|
|
124
|
+
const head = rest(label.slice(0, at));
|
|
125
|
+
const tail = rest(label.slice(at + brand.length));
|
|
126
|
+
return `${head}${theme.style(brand, { fg: BRAND_BLUE })}${tail}`;
|
|
71
127
|
}
|
|
72
128
|
|
|
73
129
|
export function panelLines(
|
|
@@ -111,7 +167,7 @@ export function panelLines(
|
|
|
111
167
|
);
|
|
112
168
|
|
|
113
169
|
// 4. Model prices with the active tariff highlighted.
|
|
114
|
-
lines.push(`${
|
|
170
|
+
lines.push(`${brandLabel(theme, text.prices, text.pricesBrand, yellow)} ${white(text.pricesState(state))}`);
|
|
115
171
|
const value = (amount: number) => amount.toFixed(4).padStart(8);
|
|
116
172
|
for (const [name, model] of Object.entries(pricing.models)) {
|
|
117
173
|
const id = `${config.providerPrefix}${name}`;
|