pi-deepseek-tariff 0.3.1 → 0.3.3

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 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
- ● DeepSeek Off-peak for 11h 59m ← footer line, always visible
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.3.0
41
+ pi install git:github.com/valderan/pi-deepseek-tariff@v0.3.3
42
42
 
43
43
  # local checkout, for development
44
44
  git clone git@github.com:valderan/pi-deepseek-tariff.git
@@ -62,16 +62,18 @@ Then run `/dsp`. Afterwards `pi list`, `pi update --extensions`, `pi remove …`
62
62
  | `/dsp en` / `ru` | switch the panel language (persisted) |
63
63
  | `/dsp footer on` / `off` (or `/dsp footeron` / `footeroff`) | show or hide the footer line (persisted) |
64
64
 
65
+ Type `/dsp ` and press Tab: the arguments above are offered as completions, with a short description each (`/dsp footer <Tab>` completes `on` and `off`).
66
+
65
67
  ## What it does
66
68
 
67
- - **Footer line** — one compact status line: `● DeepSeek Off-peak for 11h 59m`, or `● DeepSeek Peak for 1h 40m` during peak. The text is always yellow and only the dot changes colour — green for off-peak, red for peak. Shown only while a DeepSeek model is active; `/dsp footer off` hides it for good.
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.
68
70
 
69
- Russian looks like `● DeepSeek Обычный ещё 11ч 59м` / `● DeepSeek Пиковый ещё 1ч 40м`.
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м`.
70
72
 
71
73
  Pi renders every extension status on a single footer line, joined alphabetically, so the segment is kept short.
72
74
  - **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.
73
75
  - **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.
74
- - **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.
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, and the `DeepSeek` word in the `DeepSeek prices:` label keeps the same brand blue as the footer.
75
77
  - **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.
76
78
 
77
79
  ## Configuration
@@ -134,11 +136,11 @@ the commit whose `package.json` says `X.Y.Z`, and npm publishes that same versio
134
136
 
135
137
  ```bash
136
138
  # 1. bump package.json, add a CHANGELOG entry, commit
137
- npm version 0.3.2 --no-git-tag-version # or edit by hand
138
- git commit -am "chore: release 0.3.2"
139
+ npm version 0.3.3 --no-git-tag-version # or edit by hand
140
+ git commit -am "chore: release 0.3.3"
139
141
 
140
142
  # 2. tag the release commit and push
141
- git tag -a v0.3.2 -m v0.3.2
143
+ git tag -a v0.3.3 -m v0.3.3
142
144
  git push origin main --tags
143
145
 
144
146
  # 3. publish the tag to npm (2FA prompt, or --otp=123456)
@@ -149,7 +151,7 @@ npm publish --access public
149
151
  Check the result:
150
152
 
151
153
  ```bash
152
- npm view pi-deepseek-tariff dist-tags # { latest: '0.3.2' }
154
+ npm view pi-deepseek-tariff dist-tags # { latest: '0.3.3' }
153
155
  npm view pi-deepseek-tariff versions
154
156
  ```
155
157
 
@@ -157,7 +159,7 @@ npm view pi-deepseek-tariff versions
157
159
  `NPM_TOKEN` secret is configured; with trusted publishing enabled on npmjs.com the
158
160
  token is not needed at all and the build is signed with provenance.
159
161
 
160
- Tags are pinnable with `pi install git:…@v0.3.2`. Published versions show up in the
162
+ Tags are pinnable with `pi install git:…@v0.3.3`. Published versions show up in the
161
163
  [pi package gallery](https://pi.dev/packages) through the `pi-package` keyword.
162
164
 
163
165
  ## Changelog
@@ -166,11 +166,37 @@ export default function deepseekPeak(pi: ExtensionAPI) {
166
166
  render(ctx);
167
167
  };
168
168
 
169
+ // Tab completion for the documented arguments, so `/dsp <TAB>` lists what the
170
+ // handler actually accepts instead of leaving them buried in the README.
171
+ const ARGUMENTS = [
172
+ { value: "on", label: "on", description: "Show the panel · Показать панель" },
173
+ { value: "off", label: "off", description: "Hide the panel · Скрыть панель" },
174
+ { value: "refresh", label: "refresh", description: "Re-parse prices now · Обновить цены сейчас" },
175
+ { value: "en", label: "en", description: "Panel language: English · Язык панели: английский" },
176
+ { value: "ru", label: "ru", description: "Panel language: Russian · Язык панели: русский" },
177
+ { value: "footer", label: "footer", description: "Footer line: on|off · Строка футера: on|off" },
178
+ ];
179
+ const FOOTER_ARGUMENTS = ARGUMENTS.filter((item) => item.value === "on" || item.value === "off");
180
+
181
+ const getArgumentCompletions = (prefix: string) => {
182
+ const words = prefix.trimStart().split(/\s+/);
183
+ if (words.length > 2) return null;
184
+ const [head, tail] = words;
185
+ if (head === undefined || head === "") return ARGUMENTS;
186
+ if (tail === undefined) {
187
+ const matches = ARGUMENTS.filter((item) => item.value.startsWith(head.toLowerCase()));
188
+ return matches.length > 0 ? matches : null;
189
+ }
190
+ if (head.toLowerCase() !== "footer") return null;
191
+ const matches = FOOTER_ARGUMENTS.filter((item) => item.value.startsWith(tail.toLowerCase()));
192
+ return matches.length > 0 ? matches : null;
193
+ };
194
+
169
195
  const description = "DeepSeek peak/off-peak panel · Панель DeepSeek peak/off-peak";
170
196
  // `/dsp` is the short form; `deepseek-peek` accepts the spelling people reach
171
197
  // for when they type "peak" wrong, so the command still resolves.
172
198
  for (const name of ["deepseek-peak", "dsp", "deepseek-peek"]) {
173
- pi.registerCommand(name, { description, handler: commandHandler });
199
+ pi.registerCommand(name, { description, handler: commandHandler, getArgumentCompletions });
174
200
  }
175
201
  }
176
202
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-deepseek-tariff",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "DeepSeek peak/off-peak tariff panel for pi: local clock, next window, live per-model prices and Search & Fetch rates",
5
5
  "keywords": [
6
6
  "pi-package",
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: "DeepSeek",
68
- statusPeak: "Peak",
69
- statusOffPeak: "Off-peak",
70
- statusFor: (duration) => `for ${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: "DeepSeek",
96
- statusPeak: "Пиковый",
97
- statusOffPeak: "Обычный",
98
- statusFor: (duration) => `ещё ${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/view.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
2
  * Presentation: the footer status line and the panel body.
3
3
  *
4
- * Colors follow the pi theme: labels in the warning token, values in the text
5
- * token, active rows bold, inactive rows dim.
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 { parseColor } 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";
@@ -26,6 +29,12 @@ import {
26
29
  /** Placeholder for a full-width rule; the renderer expands it to the panel width. */
27
30
  export const SEPARATOR = "\u0000rule";
28
31
 
32
+ /** Footer clock: one cell wide, so the segment stays short on the shared status line. */
33
+ const CLOCK = "\u25F7"; // ◷
34
+
35
+ /** DeepSeek brand blue. Fixed in every theme: it is the product colour, not a theme role. */
36
+ const BRAND_BLUE = parseColor("#4175E6");
37
+
29
38
  /** "Search & Fetch" value: a shared price is printed once. */
30
39
  export function requestPricingText(entries: RequestPricing[], lang: Lang): string | undefined {
31
40
  if (entries.length === 0) return undefined;
@@ -54,9 +63,12 @@ export function requestPricingText(entries: RequestPricing[], lang: Lang): strin
54
63
  }
55
64
 
56
65
  /**
57
- * Compact footer line: a coloured dot plus always-yellow text, for example
58
- * "● DeepSeek Off-peak for 11h 59m" or "● DeepSeek Peak for 1h 40m".
59
- * Only the dot changes colour, so the line reads calmly in both states.
66
+ * Compact footer line: a state-coloured clock, the brand name in DeepSeek blue,
67
+ * the tariff in English and the countdown in the theme accent — for example
68
+ * "◷ deepseek OFF-PEAK → 11h 59m" or "◷ deepseek PEAK → 1h 40m".
69
+ *
70
+ * The clock carries the state colour, so the line stays legible without a
71
+ * separate status dot; the duration keeps the panel language units.
60
72
  */
61
73
  export function statusText(theme: Theme, config: Config, lang: Lang, pricing: Pricing): string {
62
74
  void pricing;
@@ -64,10 +76,25 @@ export function statusText(theme: Theme, config: Config, lang: Lang, pricing: Pr
64
76
  const now = new Date();
65
77
  const peak = isPeak(now, config.holidays);
66
78
  const next = nextTransition(now, config.holidays);
67
- const dot = theme.fg(peak ? "error" : "success", "●");
68
- const label = peak ? text.statusPeak : text.statusOffPeak;
79
+ const state = peak ? "error" : "success";
69
80
  const duration = humanize(next.getTime() - now.getTime(), lang);
70
- return `${dot} ${theme.fg("warning", `${text.statusBrand} ${label} ${text.statusFor(duration)}`)}`;
81
+ const clock = theme.fg(state, CLOCK);
82
+ const brand = theme.style(text.statusBrand, { fg: BRAND_BLUE });
83
+ const label = theme.fg(state, peak ? text.statusPeak : text.statusOffPeak);
84
+ return `${clock} ${brand} ${label} ${theme.fg("accent", text.statusFor(duration))}`;
85
+ }
86
+
87
+ /**
88
+ * Colour the product name inside a panel label without splitting the string in
89
+ * i18n: the label stays one translatable line, only the brand word is swapped to
90
+ * the DeepSeek blue. Falls back to `rest(label)` when the name is missing.
91
+ */
92
+ function brandLabel(theme: Theme, label: string, brand: string, rest: (value: string) => string): string {
93
+ const at = label.indexOf(brand);
94
+ if (at < 0) return rest(label);
95
+ const head = rest(label.slice(0, at));
96
+ const tail = rest(label.slice(at + brand.length));
97
+ return `${head}${theme.style(brand, { fg: BRAND_BLUE })}${tail}`;
71
98
  }
72
99
 
73
100
  export function panelLines(
@@ -111,7 +138,7 @@ export function panelLines(
111
138
  );
112
139
 
113
140
  // 4. Model prices with the active tariff highlighted.
114
- lines.push(`${yellow(text.prices)} ${white(text.pricesState(state))}`);
141
+ lines.push(`${brandLabel(theme, text.prices, text.pricesBrand, yellow)} ${white(text.pricesState(state))}`);
115
142
  const value = (amount: number) => amount.toFixed(4).padStart(8);
116
143
  for (const [name, model] of Object.entries(pricing.models)) {
117
144
  const id = `${config.providerPrefix}${name}`;