rinco-pi-hud 1.1.1 → 1.3.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 CHANGED
@@ -2,49 +2,36 @@
2
2
 
3
3
  # Rinco Pi HUD
4
4
 
5
- **A dynamic HUD footer for [Pi](https://github.com/earendil-works/pi).**
5
+ **A live status footer for [Pi](https://github.com/earendil-works/pi).** Project, git, session, tool and cost state in four groups, updated while you work.
6
6
 
7
7
  [![Pi package](https://img.shields.io/badge/Pi-package-F2A7C6?style=flat-square)](https://github.com/earendil-works/pi) [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![Tests](https://img.shields.io/badge/tests-Vitest-6E9F18?style=flat-square&logo=vitest&logoColor=white)](https://vitest.dev/) [![License](https://img.shields.io/badge/license-MIT-C7B8F5?style=flat-square)](LICENSE)
8
8
 
9
- [Features](#features) · [Installation](#installation) · [Usage](#usage) · [Customization](#customization) · [Development](#development)
9
+ [Install](#install) · [Commands](#commands) · [What it shows](#what-it-shows) · [Layout](#layout) · [Development](#development)
10
10
 
11
11
  [简体中文](docs/README.zh-CN.md)
12
12
 
13
13
  </div>
14
14
 
15
- Rinco Pi HUD provides a rich, real-time status footer for Pi TUI sessions. It displays project state, Git metrics, runtime versions, session activity, model usage, token costs, and more — all in a responsive four-group or customizable single-line layout. Pi loads the TypeScript extension directly, so no build step is required (`tsconfig.json` sets `noEmit: true` and is used only for `npm run typecheck`).
15
+ <p align="center">
16
+ <img src="./assets/readme/hero.svg" width="100%" alt="The Rinco Pi HUD footer inside Pi: a Project row with directory, git branch and Node version, a Session row with provider and model, an Activity row with tool counts, and a Usage row with context window, tokens, cache hit rate and cost.">
17
+ </p>
16
18
 
17
- ## Features
19
+ Rinco Pi HUD takes over Pi's footer with four groups of live state and lets you switch off anything you do not want. Pi loads the TypeScript extension directly, so there is no build step: `tsconfig.json` sets `noEmit: true` and exists only for `npm run typecheck`.
18
20
 
19
- - **Project state** — Working directory, Git branch, commit, tag, status, operation state, diff metrics, and runtime versions.
20
- - **Session activity** — Model, provider, context usage, independently controlled input/output tokens, cache, session cost, turn count, and thinking level.
21
- - **Tool and agent activity** — Independent controls for running tools, completed tool counts, active agents, idle state, skills, and MCP server status.
22
- - **Model quota** — Automatic Codex weekly quota or Token Switch balance display by model provider.
23
- - **Git awareness** — Branches, detached HEAD, tags, ahead/behind, stash, merge/rebase conflicts, and dirty state.
24
- - **Runtime detection** — Node, Python, Go, Rust, Java, and 60+ other runtimes with package version parsing.
25
- - **Configurable layout** — Responsive Project, Session, Activity, and Usage groups by default, or fully customizable single-line templates.
26
- - **Granular status controls** — Toggle Git operation state, tool runtime/counts, active/idle agents, and input/output tokens separately from `/zentui`.
27
- - **Extension statuses** — Reads third-party extension statuses and places them by configurable position and color mode.
28
- - **Safe fallback** — Graceful timeout and error handling; no stale data after session shutdown.
29
-
30
- ## Installation
31
-
32
- ### Install from npm (recommended)
21
+ ## Install
33
22
 
34
23
  ```bash
35
24
  pi install npm:rinco-pi-hud
36
25
  ```
37
26
 
38
- The published package is available on [npm](https://www.npmjs.com/package/rinco-pi-hud).
27
+ Restart Pi afterwards, or run `/reload`. The footer is enabled by default and `/zentui` opens the settings UI.
39
28
 
40
- ### Install from GitHub
29
+ From GitHub, or from a local checkout:
41
30
 
42
31
  ```bash
43
32
  pi install git:github.com/Rinisnotarobot/rinco-pi-hud
44
33
  ```
45
34
 
46
- ### Install from a local checkout
47
-
48
35
  ```bash
49
36
  git clone https://github.com/Rinisnotarobot/rinco-pi-hud.git
50
37
  cd rinco-pi-hud
@@ -54,34 +41,62 @@ pi install "$PWD"
54
41
  > [!IMPORTANT]
55
42
  > Pi extensions run with your user permissions. Review the source before installing any third-party extension.
56
43
 
57
- ## Usage
44
+ > [!NOTE]
45
+ > The footer needs a Truecolor terminal. The screenshot above renders with ASCII icon mode; `/zentui` switches between the `auto`, `nerd`, and `ascii` glyph sets.
58
46
 
59
- Restart Pi after installation, or run:
47
+ ## Commands
60
48
 
61
- ```text
62
- /reload
63
- ```
49
+ | Command | Effect |
50
+ | --- | --- |
51
+ | `/zentui` | Settings UI: rows, segments, separator, icons, colors, templates |
52
+ | `/zentui statusline enable\|disable\|toggle` | Show or hide the whole footer |
53
+ | `/zentui row project\|session\|activity\|usage <enable\|disable\|toggle>` | Turn one row on or off |
54
+ | `/zentui format "<template>"` | Replace the four rows with a single-line template |
55
+ | `/codex-status` | Print Codex subscription usage and rate-limit windows |
56
+ | `/usage-refresh` | Refresh the model usage or balance shown in the footer |
57
+
58
+ ## What it shows
59
+
60
+ | Row | Segments, each with its own switch under **Built-in segments** in `/zentui` |
61
+ | --- | --- |
62
+ | **Project** | directory, git branch, commit and tag, working-tree status, operation state (`MERGING`, `REBASING`, …), diff metrics, runtime, package version, OS, user |
63
+ | **Session** | session name, provider and model, thinking level, turn count, duration |
64
+ | **Activity** | running tools, completed tool counts, active agents, `Agent idle`, skills, MCP servers |
65
+ | **Usage** | context window with a gauge, input and output tokens, cache read/write volume and hit rate, cost, model quota, clock |
66
+
67
+ Rows are toggled with `/zentui row usage disable`; the segments inside them have individual switches in the same UI. Existing configs that used the former aggregate `tokens`, `toolActivity`, or `agentActivity` switches are migrated automatically.
68
+
69
+ ### Model quota
70
+
71
+ The Usage row follows the active model's provider:
72
+
73
+ - **openai-codex** — ChatGPT subscription rate limits and reset credits, queried through Pi auth with a `codex app-server` fallback. Reads as `codex 60% 5h (14:30) 75% wk`; the reset stamp becomes `14:30 12 Feb` when the window rolls into another day.
74
+ - **token-switch** — billing balance from the `TOKEN_SWITCH_API_KEY` environment variable, as `token-switch $750.00`.
75
+ - **deepseek** — account balance from the DeepSeek API using the credential Pi holds for the provider, either from `/login` or `DEEPSEEK_API_KEY`. Reads as `deepseek ¥110.00`, preferring CNY when the account reports several currencies.
64
76
 
65
- The HUD footer is enabled by default. Run `/zentui` to open the interactive settings UI.
77
+ These queries use a 15-second timeout and a 5-minute cache, and `/usage-refresh` refreshes the active one on demand.
66
78
 
67
- ### Common commands
79
+ ## Layout
80
+
81
+ <p align="center">
82
+ <img src="./assets/readme/responsive.svg" width="100%" alt="The same footer rendered at 72 and 56 columns: rows wrap between segments, continuation lines realign under the separator column, and a tool-count segment that cannot fit is shortened with an ellipsis while the segments after it still render.">
83
+ </p>
84
+
85
+ Groups wrap only at segment boundaries. A continuation line realigns under the separator column, and a segment that no longer fits is shortened with `…` instead of pushing the segments after it off the row.
86
+
87
+ Set a template for a single-line footer instead of the four groups:
68
88
 
69
89
  ```text
70
- /zentui statusline enable
71
- /zentui statusline disable
72
- /zentui statusline toggle
73
- /zentui row project toggle
74
- /zentui row session disable
75
- /zentui row activity enable
76
- /zentui row usage toggle
77
- /zentui format clear
78
- /zentui format "$model · $context · $cost · $git_branch( $git_commit) · $session_duration"
79
- /codex-status
80
- /usage-refresh
90
+ /zentui format "$cwd( $git_branch)$fill($context)( $tokens)( $cost)"
81
91
  ```
82
92
 
83
- > [!NOTE]
84
- > For the best result, use a Truecolor terminal and a Nerd Font. The footer uses Unicode glyphs that render best with proper font support.
93
+ Variables accept `$name` or `${name}`. The [footer reference](docs/footer.md) lists all of them, along with the aliases (`$directory`, `$status`, `$codex`, …). Run `/zentui format` with no argument, or `/zentui format clear`, to go back to the four groups.
94
+
95
+ ## Configuration
96
+
97
+ `/zentui` writes to `~/.pi/agent/rinco-pi-hud.json` and applies changes immediately. It covers the separator style, icon mode (`nerd`, `ascii`, or `auto`), footer colors (Pi theme tokens or terminal palette styles), context rendering (text, gauge, or both), the project refresh interval, and third-party extension statuses with their own placement and color mode.
98
+
99
+ See [docs/footer.md](docs/footer.md) for the full variable and segment reference.
85
100
 
86
101
  ## Project contents
87
102
 
@@ -91,119 +106,66 @@ The HUD footer is enabled by default. Run `/zentui` to open the interactive sett
91
106
  | [`extensions/hud/config/`](extensions/hud/config/config.ts) | Configuration model, normalization, migration, and persistence |
92
107
  | [`extensions/hud/footer/`](extensions/hud/footer/index.ts) | Footer rendering, semantic grouping, responsive layout, and template parsing |
93
108
  | [`extensions/hud/segments/`](extensions/hud/segments/) | State collectors: Git, runtime, MCP, skills, projects, etc. |
94
- | [`extensions/hud/telemetry/`](extensions/hud/telemetry/format.ts) | Usage formatting, token switch, and Codex subscription client |
109
+ | [`extensions/hud/telemetry/`](extensions/hud/telemetry/format.ts) | Usage formatting, provider balance probes, and the Codex subscription client |
95
110
  | [`extensions/hud/session/`](extensions/hud/session/) | Pi event registration, session lifecycle, and live context overlay |
96
111
  | [`extensions/hud/state/`](extensions/hud/state/) | Aggregated state, telemetry reducer, and project refresh controller |
97
112
  | [`extensions/hud/commands/`](extensions/hud/commands/settings.ts) | `/zentui` settings UI and configuration controller |
98
113
  | [`extensions/hud/ui/`](extensions/hud/ui/) | Icons and terminal style utilities |
99
114
  | [`docs/footer.md`](docs/footer.md) | Complete footer configuration reference |
100
115
  | [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) | Local development, testing guidance, and pull request checklist |
101
- | [`tests/`](tests/) | Vitest test suite (41 tests across 8 files) |
102
-
103
- ## Customization
104
-
105
- ### Layout
106
-
107
- The default layout groups Project, Session, Activity, and Usage information. Long groups wrap at complete segment boundaries with aligned continuation lines. Set `footerFormat` to a template string for a single-line layout:
108
-
109
- ```text
110
- /zentui format "$cwd( $git_branch)$fill($context)( $tokens)( $cost)"
111
- ```
112
-
113
- Template variables use `$name` or `${name}` syntax. See the [footer reference](docs/footer.md) for the complete variable list.
114
-
115
- ### Segments
116
-
117
- Open `/zentui`, then select **Built-in segments** to enable or disable individual statuses. Fine-grained controls include:
118
-
119
- | Group | Independently controlled statuses |
120
- | --- | --- |
121
- | Project | Git status, Git operation state (`MERGING`, `REBASING`, etc.), commit, metrics, runtime, package, OS, and user |
122
- | Session | Model, thinking level, turn count, and duration |
123
- | Activity | Running tools, completed tool counts, active agents, `Agent idle`, skills, and MCP |
124
- | Usage | Context, input tokens, output tokens, cache details/hit rate, cost, quota, and time |
125
-
126
- Existing configs using the former aggregate `tokens`, `toolActivity`, or `agentActivity` switches are migrated automatically. The whole footer can still be controlled with `/zentui statusline enable|disable|toggle`.
127
-
128
- In the default four-row layout, each semantic row can also be controlled directly:
129
-
130
- ```text
131
- /zentui row <project|session|activity|usage> <enable|disable|toggle>
132
- ```
133
-
134
- Row switches are persisted in `footerRows`. They do not affect a custom single-line `footerFormat`.
135
-
136
- ### Extension statuses
137
-
138
- Third-party extensions publish statuses that the HUD footer can display. Configure placement (`left`, `middle`, `right`, `off`) and color mode (`zentui`, `original`) for each extension status key.
116
+ | [`tests/`](tests/) | Vitest suite: footer layout, telemetry, provider balances, MCP parsing |
139
117
 
140
118
  ## Development
141
119
 
142
- ### Prerequisites
143
-
144
- - [Node.js](https://nodejs.org/) 22.19 or later
145
- - [Pi](https://github.com/earendil-works/pi)
146
-
147
- Install development dependencies:
120
+ Node.js 22.19 or later, and [Pi](https://github.com/earendil-works/pi) itself.
148
121
 
149
122
  ```bash
150
123
  npm install
124
+ npm test # Vitest suite
125
+ npm run typecheck # tsc --noEmit
126
+ npm run verify # format, types, lint, tests
151
127
  ```
152
128
 
153
- ### Available commands
154
-
155
- <!-- AUTO-GENERATED: package-scripts:start -->
156
- <!-- Source: package.json#scripts. Do not edit manually. -->
157
-
158
- | Command | Description |
159
- | --- | --- |
160
- | `npm test` | Run the complete Vitest suite once. |
161
- | `npm run test:watch` | Run Vitest in watch mode and retest when files change. |
162
- | `npm run typecheck` | Run TypeScript type checking with `tsc --noEmit`. |
163
- | `npm run pack:check` | Inspect the publishable package with `npm pack --dry-run` without publishing it. |
164
-
165
- <!-- AUTO-GENERATED: package-scripts:end -->
166
-
167
- Run the test suite:
129
+ Every script is defined in [`package.json`](package.json); [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) lists them all, including `lint`, `format`, and `test:watch`.
168
130
 
169
- ```bash
170
- npm test
171
- ```
131
+ ### README images
172
132
 
173
- Check types:
133
+ The two boards above are generated from a real footer render rather than drawn by hand. `assets/readme/source/capture-footer.mts` drives the extension's own `installFooter` path over this repository's state and a real Pi session transcript, then `build-assets.mts` turns the ANSI rows into SVG:
174
134
 
175
135
  ```bash
176
- npm run typecheck
136
+ npx tsx assets/readme/source/capture-footer.mts . ~/.pi/agent/sessions/<session>.jsonl 118 72 56 > /tmp/footer.json
137
+ npx tsx assets/readme/source/build-assets.mts /tmp/footer.json
177
138
  ```
178
139
 
179
- Verify formatting, types, lint rules, tests, and package contents:
180
-
181
- ```bash
182
- npm run verify
183
- npm run pack:check
184
- ```
140
+ ## Troubleshooting
185
141
 
186
- Vitest covers footer layout, telemetry, codex usage, token switch, config counts, and MCP status parsing. Pi loads the TypeScript source directly, so this project has no separate build command.
142
+ <details>
143
+ <summary>The footer does not appear</summary>
187
144
 
188
- ## Troubleshooting
145
+ Confirm Pi is running in TUI mode rather than headless, JSON, or print mode. Then run `/reload`, and `pi list` to confirm the package is installed.
146
+ </details>
189
147
 
190
- ### The footer does not appear
148
+ <details>
149
+ <summary>The layout looks wrong in a narrow terminal</summary>
191
150
 
192
- 1. Confirm that Pi is running in TUI mode rather than headless, JSON, or print mode.
193
- 2. Run `/reload` or restart Pi.
194
- 3. Run `pi list` to confirm that the package is installed.
151
+ The footer measures the terminal and wraps at segment boundaries, so narrow windows show fewer segments per line. Set a `footerFormat` template, or disable low-priority segments in `/zentui`.
152
+ </details>
195
153
 
196
- ### The footer layout looks wrong
154
+ <details>
155
+ <summary>Colors look incorrect</summary>
197
156
 
198
- The footer auto-detects terminal width and truncates content. In narrow terminals, some segments may be clipped. Adjust the layout by setting a custom `footerFormat` template, or disable low-priority segments in `/zentui`.
157
+ The default palette emits `38;2;r;g;b` Truecolor sequences that terminals without Truecolor support approximate or ignore.
158
+ </details>
199
159
 
200
- ### Colors look incorrect
160
+ <details>
161
+ <summary>Icons are missing or misaligned</summary>
201
162
 
202
- Make sure your terminal supports Truecolor. The footer emits `38;2;r;g;b` ANSI sequences.
163
+ Unicode and Nerd Font glyphs vary between terminals and fonts. Install a Nerd Font, or switch to ASCII icons in `/zentui`.
164
+ </details>
203
165
 
204
- ### Unicode icons are missing or misaligned
166
+ ## License
205
167
 
206
- Unicode and Nerd Font glyphs vary between terminals and fonts. Install a Nerd Font, or switch to ASCII icon mode via `/zentui`.
168
+ MIT. See [`LICENSE`](LICENSE).
207
169
 
208
170
  ## Third-party licenses
209
171
 
@@ -216,4 +178,4 @@ This package incorporates work from the following open-source projects:
216
178
  - **pi-codex-usage** by narumiruna — MIT licensed. The Codex usage client under `extensions/hud/telemetry/codex-usage/` derives from this project.
217
179
  Source: https://github.com/narumiruna/pi-codex-usage
218
180
 
219
- See [NOTICE](NOTICE) for details.
181
+ See [NOTICE](NOTICE) for details.
@@ -0,0 +1,21 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="430" viewBox="0 0 1200 430" role="img" aria-labelledby="t d" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
2
+ <title id="t">Rinco Pi HUD — a live HUD footer for the Pi terminal agent</title>
3
+ <desc id="d">The footer from a real Pi session, rendered at 118 columns: a Project row with directory, git branch, commit and Node version; a Session row with provider, model, thinking level and turn count; an Activity row with real tool counts; and a Usage row with context window, tokens, cache hit rate and cost.</desc>
4
+ <rect width="1200" height="430" rx="26" fill="#0E0C15"/>
5
+ <g id="title-block">
6
+ <text x="56" y="72" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="18" letter-spacing="2.4" fill="#F2A7C6">PI · TUI FOOTER EXTENSION</text>
7
+ <text x="56" y="126" font-size="60" font-weight="700" fill="#F5F2F8">Rinco Pi HUD<tspan fill="#F2A7C6">.</tspan></text>
8
+ <text x="56" y="162" font-size="22" fill="#B9B2C0">Live project, session, tool and cost status in your Pi footer.</text>
9
+ </g>
10
+ <g id="panel-118">
11
+ <rect x="56" y="196" width="1088" height="178" rx="14" fill="#16131F" stroke="#2C2640"/>
12
+ <text x="80" y="219" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="18" fill="#A99BAE">extensions/hud/footer/index.ts</text>
13
+ <text x="1120" y="219" text-anchor="end" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="18" fill="#A99BAE">render(118) · ASCII icons</text>
14
+ <line x1="56" y1="230" x2="1144" y2="230" stroke="#2C2640"/>
15
+
16
+ <text x="80" y="260" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#f2a7c6" font-weight="700">Project</tspan><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#f2a7c6" font-weight="700">rinco-pi-hud</tspan><tspan fill="#716879"> › </tspan><tspan>on </tspan><tspan fill="#c7b8f5" font-weight="700">*</tspan><tspan> </tspan><tspan fill="#c7b8f5" font-weight="700">main</tspan><tspan fill="#716879"> › </tspan><tspan fill="#9fd3f2">via</tspan><tspan> </tspan><tspan fill="#AEE5C5" font-weight="700">node v24.18.0</tspan><tspan fill="#716879"> › </tspan><tspan fill="#f7eef8">linux</tspan></text>
17
+ <text x="80" y="290" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#9fd3f2">Session</tspan><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#9fd3f2">deepseek/deepseek-flash</tspan><tspan fill="#716879"> › </tspan><tspan fill="#efc3e6">Thinking level: max</tspan><tspan fill="#716879"> › </tspan><tspan fill="#efc3e6">Turn 3</tspan></text>
18
+ <text x="80" y="320" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#efc3e6">Activity</tspan><tspan fill="#716879"> › </tspan><tspan fill="#aee5c5">Tool read × 30 write × 7 edit × 13 bash × 76</tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">Agent idle</tspan></text>
19
+ <text x="80" y="350" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#aee5c5">Usage</tspan><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#aee5c5">[##--------] 17%/1.0M</tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">↑ 177k</tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">↓ 74k</tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">R 10M</tspan><tspan> </tspan><tspan fill="#aee5c5">CH 99.8%</tspan><tspan fill="#716879"> › </tspan><tspan fill="#aee5c5" font-weight="700">$ 0.204</tspan></text>
20
+ </g>
21
+ </svg>
@@ -0,0 +1,36 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="722" viewBox="0 0 1200 722" role="img" aria-labelledby="t d" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif">
2
+ <title id="t">Rinco Pi HUD width-aware footer layout</title>
3
+ <desc id="d">The same Pi session footer rendered at 72 and 56 columns. Groups wrap only at complete segment boundaries, so the Activity row keeps its tool counts together; continuation lines realign under the separator column; a segment that no longer fits is truncated with an ellipsis instead of splitting the numbers.</desc>
4
+ <rect width="1200" height="722" rx="26" fill="#0E0C15"/>
5
+ <g id="board-title">
6
+ <text x="56" y="56" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="18" letter-spacing="2.4" fill="#F2A7C6">WIDTH-AWARE LAYOUT</text>
7
+ <text x="56" y="96" font-size="30" font-weight="700" fill="#F5F2F8">Wrap between segments, shorten only what cannot fit</text>
8
+ </g>
9
+ <g id="panel-72">
10
+ <rect x="56" y="126" width="1088" height="208" rx="14" fill="#16131F" stroke="#2C2640"/>
11
+ <text x="80" y="149" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="18" fill="#A99BAE">render(72)</text>
12
+ <text x="1120" y="149" text-anchor="end" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="18" fill="#A99BAE">5 rows · continuations align</text>
13
+ <line x1="56" y1="160" x2="1144" y2="160" stroke="#2C2640"/>
14
+ <line x1="194" y1="160" x2="194" y2="334" stroke="#716879" stroke-dasharray="3 5" opacity="0.55"/>
15
+ <text x="80" y="190" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#f2a7c6" font-weight="700">Project</tspan><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#f2a7c6" font-weight="700">rinco-pi-hud</tspan><tspan fill="#716879"> › </tspan><tspan>on </tspan><tspan fill="#c7b8f5" font-weight="700">*</tspan><tspan> </tspan><tspan fill="#c7b8f5" font-weight="700">main</tspan><tspan fill="#716879"> › </tspan><tspan fill="#9fd3f2">via</tspan><tspan> </tspan><tspan fill="#AEE5C5" font-weight="700">node v24.18.0</tspan><tspan fill="#716879"> › </tspan><tspan fill="#f7eef8">linux</tspan></text>
16
+ <text x="80" y="220" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#9fd3f2">Session</tspan><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#9fd3f2">deepseek/deepseek-flash</tspan><tspan fill="#716879"> › </tspan><tspan fill="#efc3e6">Thinking level: max</tspan><tspan fill="#716879"> › </tspan><tspan fill="#efc3e6">Turn 3</tspan></text>
17
+ <text x="80" y="250" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#efc3e6">Activity</tspan><tspan fill="#716879"> › </tspan><tspan fill="#aee5c5">Tool read × 30 write × 7 edit × 13 bash × 76</tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">Agent idle</tspan></text>
18
+ <text x="80" y="280" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#aee5c5">Usage</tspan><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#aee5c5">[##--------] 17%/1.0M</tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">↑ 177k</tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">↓ 74k</tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">R 10M</tspan><tspan> </tspan><tspan fill="#aee5c5">CH 99.8%</tspan></text>
19
+ <text x="80" y="310" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#aee5c5" font-weight="700">$ 0.204</tspan></text>
20
+ </g>
21
+ <g id="panel-56">
22
+ <rect x="56" y="368" width="1088" height="298" rx="14" fill="#16131F" stroke="#2C2640"/>
23
+ <text x="80" y="391" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="18" fill="#A99BAE">render(56)</text>
24
+ <text x="1120" y="391" text-anchor="end" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="18" fill="#A99BAE">8 rows · continuations align</text>
25
+ <line x1="56" y1="402" x2="1144" y2="402" stroke="#2C2640"/>
26
+ <line x1="194" y1="402" x2="194" y2="666" stroke="#716879" stroke-dasharray="3 5" opacity="0.55"/>
27
+ <text x="80" y="432" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#f2a7c6" font-weight="700">Project</tspan><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#f2a7c6" font-weight="700">rinco-pi-hud</tspan><tspan fill="#716879"> › </tspan><tspan>on </tspan><tspan fill="#c7b8f5" font-weight="700">*</tspan><tspan> </tspan><tspan fill="#c7b8f5" font-weight="700">main</tspan></text>
28
+ <text x="80" y="462" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#9fd3f2">via</tspan><tspan> </tspan><tspan fill="#AEE5C5" font-weight="700">node v24.18.0</tspan><tspan fill="#716879"> › </tspan><tspan fill="#f7eef8">linux</tspan></text>
29
+ <text x="80" y="492" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#9fd3f2">Session</tspan><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#9fd3f2">deepseek/deepseek-flash</tspan></text>
30
+ <text x="80" y="522" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#efc3e6">Thinking level: max</tspan><tspan fill="#716879"> › </tspan><tspan fill="#efc3e6">Turn 3</tspan></text>
31
+ <text x="80" y="552" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#efc3e6">Activity</tspan><tspan fill="#716879"> › </tspan><tspan fill="#aee5c5">Tool read × 30 write × 7 edit × 13 bash × </tspan><tspan>…</tspan></text>
32
+ <text x="80" y="582" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">Agent idle</tspan></text>
33
+ <text x="80" y="612" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#aee5c5">Usage</tspan><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#aee5c5">[##--------] 17%/1.0M</tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">↑ 177k</tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">↓ 74k</tspan></text>
34
+ <text x="80" y="642" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, 'Liberation Mono', monospace" font-size="20" xml:space="preserve"><tspan> </tspan><tspan fill="#716879"> › </tspan><tspan fill="#a99bae">R 10M</tspan><tspan> </tspan><tspan fill="#aee5c5">CH 99.8%</tspan><tspan fill="#716879"> › </tspan><tspan fill="#aee5c5" font-weight="700">$ 0.204</tspan></text>
35
+ </g>
36
+ </svg>
@@ -22,17 +22,20 @@ Pi loads the TypeScript extension directly, so this project has no separate buil
22
22
 
23
23
  ## Available commands
24
24
 
25
- <!-- AUTO-GENERATED: package-scripts:start -->
26
- <!-- Source: package.json#scripts. Do not edit manually. -->
27
-
28
25
  | Command | Description |
29
26
  | --- | --- |
30
27
  | `npm test` | Run the complete Vitest suite once. |
31
28
  | `npm run test:watch` | Run Vitest in watch mode and retest when files change. |
32
29
  | `npm run typecheck` | Run TypeScript type checking with `tsc --noEmit`. |
30
+ | `npm run lint` | Run ESLint over `extensions/` and `tests/`. |
31
+ | `npm run lint:fix` | Run ESLint and apply its automatic fixes. |
32
+ | `npm run format:check` | Check Prettier formatting for the TypeScript sources and the two config files. |
33
+ | `npm run format` | Apply Prettier formatting to the same files. |
34
+ | `npm run verify` | Run `format:check`, `typecheck`, `lint`, and `test`, in that order. |
33
35
  | `npm run pack:check` | Inspect the publishable package with `npm pack --dry-run` without publishing it. |
34
36
 
35
- <!-- AUTO-GENERATED: package-scripts:end -->
37
+ This table mirrors `package.json#scripts`. Nothing in this repository generates it, so update it by hand
38
+ when a script is added, renamed, or removed.
36
39
 
37
40
  ## Testing
38
41
 
@@ -56,6 +59,7 @@ Tests live in `tests/`:
56
59
  - `model-switch-statusline.test.ts` — Model switch statusline integration.
57
60
  - `model-usage-refresh.test.ts` — Model usage refresh integration.
58
61
  - `token-switch-usage.test.ts` — Token Switch balance formatting.
62
+ - `deepseek-balance.test.ts` — DeepSeek balance parsing, formatting, and query safety.
59
63
 
60
64
  When adding tests:
61
65
 
@@ -83,9 +87,7 @@ Follow the configured conventions:
83
87
  - Use TypeScript ESM and explicit types.
84
88
  - Use tab indentation, double quotes, and semicolons.
85
89
  - Keep extension lifecycle handling clear and release timers and UI state during `session_shutdown`.
86
- - Do not edit content enclosed by `AUTO-GENERATED` markers manually; regenerate it from the source identified in the marker.
87
-
88
- Do not add ignore rules, skip tests, or weaken types merely to bypass a check.
90
+ - Do not add ignore rules, skip tests, or weaken types merely to bypass a check.
89
91
 
90
92
  ## Architecture notes
91
93
 
@@ -98,9 +100,14 @@ The HUD extension is organized into functional modules under `extensions/hud/`:
98
100
  - **`segments/`** — State collectors: Git, runtime, MCP, skills, projects, etc.
99
101
  - **`session/`** — Pi event registration, lifecycle generations, and live-context throttling.
100
102
  - **`state/`** — Shared footer state, telemetry reducer, and project-refresh controller.
101
- - **`telemetry/`** — Usage formatting, token switch, and Codex subscription client.
103
+ - **`telemetry/`** — Usage formatting, provider balance probes, and Codex subscription client.
102
104
  - **`ui/`** — Icons and terminal style utilities.
103
105
 
106
+ `assets/readme/` holds the README visuals. Both boards are generated, not drawn: `source/capture-footer.mts`
107
+ runs the real `installFooter` render path over a checkout plus a real Pi session transcript, and
108
+ `source/build-assets.mts` converts the ANSI rows into SVG. Regenerate them with `npx tsx` as described in
109
+ the README instead of editing the `.svg` files by hand.
110
+
104
111
  Key conventions:
105
112
 
106
113
  - Keep composition in `index.ts`, Pi event listeners in `session/event-handlers.ts`, and avoid file reads or subprocesses in `render()`.
@@ -2,49 +2,36 @@
2
2
 
3
3
  # Rinco Pi HUD
4
4
 
5
- **为 [Pi](https://github.com/earendil-works/pi) 打造的动态 HUD 脚注。**
5
+ **为 [Pi](https://github.com/earendil-works/pi) 打造的实时状态脚注。** 项目、Git、会话、工具与费用状态分成四组,随你的操作即时更新。
6
6
 
7
7
  [![Pi package](https://img.shields.io/badge/Pi-package-F2A7C6?style=flat-square)](https://github.com/earendil-works/pi) [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![Tests](https://img.shields.io/badge/tests-Vitest-6E9F18?style=flat-square&logo=vitest&logoColor=white)](https://vitest.dev/) [![License](https://img.shields.io/badge/license-MIT-C7B8F5?style=flat-square)](../LICENSE)
8
8
 
9
- [功能](#功能) · [安装](#安装) · [使用](#使用) · [定制](#定制) · [开发](#开发)
9
+ [安装](#安装) · [命令](#命令) · [显示什么](#显示什么) · [布局](#布局) · [开发](#开发)
10
10
 
11
11
  [English](../README.md)
12
12
 
13
13
  </div>
14
14
 
15
- Rinco Pi HUD 为 Pi TUI 会话提供丰富的实时状态脚注,可显示项目状态、Git 指标、运行时版本、会话活动、模型用量、Token 费用等信息,支持响应式四分组布局或可定制的单行布局。Pi 可直接加载 TypeScript 扩展,无需构建步骤(`tsconfig.json` 设置了 `noEmit: true`,仅用于 `npm run typecheck` 类型检查)。
15
+ <p align="center">
16
+ <img src="../assets/readme/hero.svg" width="100%" alt="Rinco Pi HUD 在 Pi 中的实际脚注:Project 行显示目录、Git 分支与 Node 版本,Session 行显示 Provider 与模型,Activity 行显示工具调用计数,Usage 行显示 Context 窗口、Token、缓存命中率与费用。">
17
+ </p>
16
18
 
17
- ## 功能
18
-
19
- - **Project state**:当前目录、Git 分支、Commit、Tag、工作区状态、Git 操作状态、差异指标和运行时版本。
20
- - **Session activity**:模型、Provider、Context 用量、可独立控制的输入/输出 Token、缓存、会话费用、轮次和 Thinking 等级。
21
- - **Tool and agent activity**:可分别控制正在运行的 Tool、已完成 Tool 计数、活跃 Agent、空闲状态、Skill 和 MCP Server 状态。
22
- - **Model quota**:根据当前模型自动切换 Codex 周限额或 Token Switch 余额。
23
- - **Git awareness**:分支、Detached HEAD、Tag、Ahead/Behind、Stash、合并冲突和脏状态。
24
- - **Runtime detection**:支持 Node、Python、Go、Rust、Java 等 60+ 运行时,并解析项目清单版本。
25
- - **Configurable layout**:默认使用响应式 Project、Session、Activity、Usage 四分组布局,或完全自定义的单行模板。
26
- - **Granular status controls**:可在 `/zentui` 中分别控制 Git 操作状态、Tool 运行状态/计数、Agent 活跃/空闲状态及输入/输出 Token。
27
- - **Extension statuses**:读取第三方扩展发布的状态,按配置的位置和颜色模式显示。
28
- - **Safe fallback**:超时和错误处理优雅,会话关闭后不留过期数据。
19
+ Rinco Pi HUD 用四组实时状态替换 Pi 的脚注行,不需要的部分都能单独关掉。Pi 直接加载 TypeScript 扩展,没有构建步骤:`tsconfig.json` 设了 `noEmit: true`,只服务于 `npm run typecheck`。
29
20
 
30
21
  ## 安装
31
22
 
32
- ### 从 npm 安装(推荐)
33
-
34
23
  ```bash
35
24
  pi install npm:rinco-pi-hud
36
25
  ```
37
26
 
38
- 该包已发布至 [npm](https://www.npmjs.com/package/rinco-pi-hud)。
27
+ 装完重启 Pi,或执行 `/reload`。脚注默认启用,`/zentui` 打开设置界面。
39
28
 
40
- ### 从 GitHub 安装
29
+ 从 GitHub 或本地目录安装:
41
30
 
42
31
  ```bash
43
32
  pi install git:github.com/Rinisnotarobot/rinco-pi-hud
44
33
  ```
45
34
 
46
- ### 从本地目录安装
47
-
48
35
  ```bash
49
36
  git clone https://github.com/Rinisnotarobot/rinco-pi-hud.git
50
37
  cd rinco-pi-hud
@@ -54,36 +41,64 @@ pi install "$PWD"
54
41
  > [!IMPORTANT]
55
42
  > Pi 扩展以当前用户权限运行。安装任何第三方扩展前,请先审查其源码。
56
43
 
57
- ## 使用
44
+ > [!NOTE]
45
+ > 脚注需要支持 Truecolor 的终端。上图用的是 ASCII 图标模式,`/zentui` 可以在 `auto`、`nerd`、`ascii` 三套符号之间切换。
58
46
 
59
- 安装后重启 Pi,或执行:
47
+ ## 命令
60
48
 
61
- ```text
62
- /reload
63
- ```
49
+ | 命令 | 作用 |
50
+ | --- | --- |
51
+ | `/zentui` | 设置界面:行、状态段、分隔符、图标、颜色、模板 |
52
+ | `/zentui statusline enable\|disable\|toggle` | 显示或隐藏整个脚注 |
53
+ | `/zentui row project\|session\|activity\|usage <enable\|disable\|toggle>` | 开关某一行 |
54
+ | `/zentui format "<template>"` | 用单行模板替换四行布局 |
55
+ | `/codex-status` | 打印 Codex 订阅用量与限额窗口 |
56
+ | `/usage-refresh` | 立即刷新脚注上的模型用量或余额 |
57
+
58
+ ## 显示什么
59
+
60
+ | 行 | 状态段。每段在 `/zentui` 的 **Built-in segments** 里都有独立开关 |
61
+ | --- | --- |
62
+ | **Project** | 目录、Git 分支、Commit 与 Tag、工作区状态、Git 操作状态(`MERGING`、`REBASING` 等)、差异指标、运行时、包版本、OS、用户 |
63
+ | **Session** | 会话名称、Provider 与模型、Thinking 等级、轮次、会话时长 |
64
+ | **Activity** | 正在运行的 Tool、已完成 Tool 计数、活跃 Agent、`Agent idle`、Skill、MCP Server |
65
+ | **Usage** | Context 窗口与进度条、输入/输出 Token、缓存读写量与命中率、费用、模型限额、时间 |
66
+
67
+ 行用 `/zentui row usage disable` 开关;行内每个状态段在同一界面里各有开关。使用旧版聚合开关 `tokens`、`toolActivity`、`agentActivity` 的配置会自动迁移。
68
+
69
+ ### 模型限额
70
+
71
+ Usage 行跟随当前模型的 Provider:
72
+
73
+ - **openai-codex**:ChatGPT 订阅限额与重置额度,优先走 Pi 认证,失败时回落到 `codex app-server`。显示形如 `codex 60% 5h (14:30) 75% wk`,窗口跨天时重置时间变成 `14:30 12 Feb`。
74
+ - **token-switch**:从 `TOKEN_SWITCH_API_KEY` 环境变量读取账单余额,显示形如 `token-switch $750.00`。
75
+ - **deepseek**:用 Pi 为 deepseek 供应商保存的凭据(`/login` 写入的,或 `DEEPSEEK_API_KEY`)查询 DeepSeek 余额接口,显示形如 `deepseek ¥110.00`;账户同时有 CNY 和 USD 时优先显示 CNY。
64
76
 
65
- HUD 脚注默认启用。运行 `/zentui` 打开交互式设置。
77
+ 这些查询超时 15 秒、缓存 5 分钟,`/usage-refresh` 可以立即刷新当前生效的那一个。
66
78
 
67
- ### 常用命令
79
+ ## 布局
80
+
81
+ <p align="center">
82
+ <img src="../assets/readme/responsive.svg" width="100%" alt="同一个脚注在 72 列和 56 列终端下的渲染:行只在状态段边界换行,续行在分隔符列对齐,放不下的工具计数状态段用省略号截断,其后的状态段照常显示。">
83
+ </p>
84
+
85
+ 分组只在状态段边界换行。续行重新对齐到分隔符列下方,放不下的状态段用 `…` 截断,而不是把后面的状态段挤出这一行。
86
+
87
+ 想换成单行脚注,设置模板即可:
68
88
 
69
89
  ```text
70
- /zentui statusline enable
71
- /zentui statusline disable
72
- /zentui statusline toggle
73
- /zentui row project toggle
74
- /zentui row session disable
75
- /zentui row activity enable
76
- /zentui row usage toggle
77
- /zentui format clear
78
- /zentui format "$model · $context · $cost · $git_branch( $git_commit) · $session_duration"
79
- /codex-status
80
- /usage-refresh
90
+ /zentui format "$cwd( $git_branch)$fill($context)( $tokens)( $cost)"
81
91
  ```
82
92
 
83
- > [!NOTE]
84
- > 最佳显示效果需要支持 Truecolor 的终端和 Nerd Font。脚注使用 Unicode 符号,需要合适的字体支持。
93
+ 变量可以写成 `$name` 或 `${name}`。[脚注参考](footer.md)列出了全部变量和别名(`$directory`、`$status`、`$codex` 等)。不带参数的 `/zentui format`(或 `/zentui format clear`)恢复四行分组布局。
94
+
95
+ ## 配置
96
+
97
+ `/zentui` 把配置写入 `~/.pi/agent/rinco-pi-hud.json`,改动立即生效。可配置分隔符样式、图标模式(`nerd`、`ascii`、`auto`)、脚注配色(Pi 主题色或终端调色板样式)、Context 渲染方式(文本、进度条或两者)、项目刷新间隔,以及第三方扩展状态的位置与颜色模式。
85
98
 
86
- ## 包含内容
99
+ 完整的变量与状态段参考见 [docs/footer.md](footer.md)。
100
+
101
+ ## 仓库结构
87
102
 
88
103
  | 路径 | 说明 |
89
104
  | --- | --- |
@@ -91,119 +106,66 @@ HUD 脚注默认启用。运行 `/zentui` 打开交互式设置。
91
106
  | [`extensions/hud/config/`](../extensions/hud/config/config.ts) | 配置模型、归一化、迁移与持久化 |
92
107
  | [`extensions/hud/footer/`](../extensions/hud/footer/index.ts) | 脚注渲染、语义分组、响应式布局与模板解析 |
93
108
  | [`extensions/hud/segments/`](../extensions/hud/segments/) | 状态采集器:Git、运行时、MCP、Skill、项目等 |
94
- | [`extensions/hud/telemetry/`](../extensions/hud/telemetry/format.ts) | 用量格式化、Token Switch 与 Codex 订阅客户端 |
109
+ | [`extensions/hud/telemetry/`](../extensions/hud/telemetry/format.ts) | 用量格式化、各家余额探测与 Codex 订阅客户端 |
95
110
  | [`extensions/hud/session/`](../extensions/hud/session/) | Pi 事件注册、会话生命周期与实时 Context 覆盖 |
96
111
  | [`extensions/hud/state/`](../extensions/hud/state/) | 聚合状态、Telemetry Reducer 与项目刷新 Controller |
97
112
  | [`extensions/hud/commands/`](../extensions/hud/commands/settings.ts) | `/zentui` 设置界面与配置 Controller |
98
113
  | [`extensions/hud/ui/`](../extensions/hud/ui/) | 图标与终端样式工具 |
99
114
  | [`docs/footer.md`](footer.md) | 完整脚注配置参考 |
100
115
  | [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) | 本地开发、测试规范和 PR 检查清单 |
101
- | [`tests/`](../tests/) | Vitest 测试套件(41 个测试用例,8 个文件) |
102
-
103
- ## 定制
104
-
105
- ### 布局
106
-
107
- 默认布局将信息分为 Project、Session、Activity 和 Usage 四组;超长分组会在完整状态项边界处换行,并保持续行对齐。设置 `footerFormat` 模板字符串可切换为单行布局:
108
-
109
- ```text
110
- /zentui format "$cwd( $git_branch)$fill($context)( $tokens)( $cost)"
111
- ```
112
-
113
- 模板变量使用 `$name` 或 `${name}` 语法。完整变量列表见[脚注参考](footer.md)。
114
-
115
- ### 状态段
116
-
117
- 运行 `/zentui` 并进入 **Built-in segments**,即可启用或禁用单个状态:
118
-
119
- | 分组 | 可独立控制的状态 |
120
- | --- | --- |
121
- | Project | Git 状态、Git 操作状态(`MERGING`、`REBASING` 等)、Commit、差异指标、运行时、包版本、OS 和用户 |
122
- | Session | 模型、Thinking 等级、轮次和会话时长 |
123
- | Activity | 正在运行的 Tool、已完成 Tool 计数、活跃 Agent、`Agent idle`、Skill 和 MCP |
124
- | Usage | Context、输入 Token、输出 Token、缓存详情/命中率、费用、限额和时间 |
125
-
126
- 使用旧版聚合开关 `tokens`、`toolActivity` 或 `agentActivity` 的配置会自动迁移。仍可通过 `/zentui statusline enable|disable|toggle` 控制整个脚注。
127
-
128
- 默认四行布局中的每个语义行也可以通过命令单独控制:
129
-
130
- ```text
131
- /zentui row <project|session|activity|usage> <enable|disable|toggle>
132
- ```
133
-
134
- 行开关会持久化到 `footerRows`。使用自定义单行 `footerFormat` 时,这些开关不生效。
135
-
136
- ### 扩展状态
137
-
138
- 第三方扩展发布的状态可由 HUD 脚注显示。可为每个扩展状态键配置位置(`left`、`middle`、`right`、`off`)和颜色模式(`zentui`、`original`)。
116
+ | [`tests/`](../tests/) | Vitest 测试套件:脚注布局、遥测、各家余额与 MCP 解析 |
139
117
 
140
118
  ## 开发
141
119
 
142
- ### 环境要求
143
-
144
- - [Node.js](https://nodejs.org/) 22.19 或更高版本
145
- - [Pi](https://github.com/earendil-works/pi)
146
-
147
- 安装开发依赖:
120
+ 需要 Node.js 22.19 或更高版本,以及 [Pi](https://github.com/earendil-works/pi) 本身。
148
121
 
149
122
  ```bash
150
123
  npm install
124
+ npm test # Vitest 测试套件
125
+ npm run typecheck # tsc --noEmit
126
+ npm run verify # 格式、类型、Lint、测试
151
127
  ```
152
128
 
153
- ### 可用命令
154
-
155
- <!-- AUTO-GENERATED: package-scripts:start -->
156
- <!-- Source: package.json#scripts. Do not edit manually. -->
157
-
158
- | 命令 | 说明 |
159
- | --- | --- |
160
- | `npm test` | 使用 Vitest 运行一次完整测试套件。 |
161
- | `npm run test:watch` | 以监听模式运行 Vitest,并在文件变化后重新测试。 |
162
- | `npm run typecheck` | 使用 `tsc --noEmit` 运行 TypeScript 类型检查。 |
163
- | `npm run pack:check` | 使用 `npm pack --dry-run` 检查发布包内容,不生成正式发布。 |
164
-
165
- <!-- AUTO-GENERATED: package-scripts:end -->
166
-
167
- 运行测试:
129
+ 全部脚本定义在 [`package.json`](package.json) 里,完整清单(含 `lint`、`format`、`test:watch`)见 [docs/CONTRIBUTING.md](CONTRIBUTING.md)。
168
130
 
169
- ```bash
170
- npm test
171
- ```
131
+ ### README 图片
172
132
 
173
- 类型检查:
133
+ 上面两张图不是手绘的,而是从真实脚注渲染结果生成的。`assets/readme/source/capture-footer.mts` 用扩展自己的 `installFooter` 渲染路径跑本仓库的状态和一份真实 Pi 会话记录,`build-assets.mts` 再把 ANSI 文本转成 SVG:
174
134
 
175
135
  ```bash
176
- npm run typecheck
136
+ npx tsx assets/readme/source/capture-footer.mts . ~/.pi/agent/sessions/<session>.jsonl 118 72 56 > /tmp/footer.json
137
+ npx tsx assets/readme/source/build-assets.mts /tmp/footer.json
177
138
  ```
178
139
 
179
- 验证格式、类型、Lint、测试与发布内容:
180
-
181
- ```bash
182
- npm run verify
183
- npm run pack:check
184
- ```
140
+ ## 常见问题
185
141
 
186
- 本项目使用 Vitest 测试脚注布局、遥测、Codex 用量、Token Switch、配置统计和 MCP 状态解析。Pi 运行时会直接加载 TypeScript 源码,因此没有单独的构建命令。
142
+ <details>
143
+ <summary>脚注没有显示</summary>
187
144
 
188
- ## 常见问题
145
+ 确认当前运行在 Pi TUI 模式,而不是 Headless、JSON 或 Print 模式。然后执行 `/reload`,并用 `pi list` 确认包已安装。
146
+ </details>
189
147
 
190
- ### 脚注没有显示
148
+ <details>
149
+ <summary>窄终端里布局看起来不对</summary>
191
150
 
192
- 1. 确认当前运行在 Pi TUI 模式,而不是 Headless、JSON 或 Print 模式。
193
- 2. 执行 `/reload` 或重启 Pi。
194
- 3. 使用 `pi list` 确认包已安装。
151
+ 脚注会测量终端宽度,只在状态段边界换行,所以窗口越窄,每行容纳的状态段越少。可以设置 `footerFormat` 模板,或在 `/zentui` 中关闭低优先级状态段。
152
+ </details>
195
153
 
196
- ### 脚注布局异常
154
+ <details>
155
+ <summary>颜色显示异常</summary>
197
156
 
198
- 脚注会自动检测终端宽度并截断内容。在窄终端中,部分状态段可能被裁剪。可通过设置自定义 `footerFormat` 模板调整布局,或在 `/zentui` 中关闭低优先级段。
157
+ 默认配色输出 `38;2;r;g;b` Truecolor 序列,不支持 Truecolor 的终端会近似处理或直接忽略。
158
+ </details>
199
159
 
200
- ### 颜色显示异常
160
+ <details>
161
+ <summary>图标缺失或错位</summary>
201
162
 
202
- 确认终端已启用 Truecolor。脚注使用 `38;2;r;g;b` ANSI 序列。
163
+ Unicode 与 Nerd Font 符号的显示效果因终端和字体而异。建议安装 Nerd Font,或在 `/zentui` 中切换到 ASCII 图标。
164
+ </details>
203
165
 
204
- ### Unicode 图标缺失或错位
166
+ ## 许可证
205
167
 
206
- Unicode 和 Nerd Font 符号的显示效果因终端和字体而异。建议安装 Nerd Font,或在 `/zentui` 中切换到 ASCII 图标模式。
168
+ MIT,详见 [`LICENSE`](../LICENSE)。
207
169
 
208
170
  ## 第三方许可证
209
171
 
@@ -216,4 +178,4 @@ Unicode 和 Nerd Font 符号的显示效果因终端和字体而异。建议安
216
178
  - **pi-codex-usage**(narumiruna)— MIT 许可。`extensions/hud/telemetry/codex-usage/` 下的 Codex 用量客户端衍生自该项目。
217
179
  源码:https://github.com/narumiruna/pi-codex-usage
218
180
 
219
- 详见 [NOTICE](../NOTICE)。
181
+ 详见 [NOTICE](../NOTICE)。
package/docs/footer.md CHANGED
@@ -149,7 +149,8 @@ $separator → $sep
149
149
 
150
150
  The status line automatically switches quota display based on model provider:
151
151
 
152
- - **openai-codex**: Uses Pi Model Registry's Codex Auth to query ChatGPT usage endpoint; falls back to `codex app-server --listen stdio://`. Shows weekly limit percentage, e.g. `codex 75% wk`.
152
+ - **openai-codex**: Uses Pi Model Registry's Codex Auth to query ChatGPT usage endpoint; falls back to `codex app-server --listen stdio://`. Shows the remaining 5h window with its reset time plus the weekly limit percentage, e.g. `codex 60% 5h (14:30) 75% wk` (the reset stamp becomes `14:30 12 Feb` when the window rolls over on a later day).
153
153
  - **token-switch**: Uses `TOKEN_SWITCH_API_KEY` environment variable to query billing subscription and usage. Shows available balance, e.g. `token-switch $750.00`.
154
+ - **deepseek**: Uses the credential Pi holds for the DeepSeek provider (the one `/login` stores for `deepseek`, or `DEEPSEEK_API_KEY`) to query `https://api.deepseek.com/user/balance`. Shows the total balance, preferring CNY when the account reports several currencies, e.g. `deepseek ¥110.00`. A balance query without a usable key reads as `balance error` in the footer.
154
155
 
155
- Both queries use 15-second timeout, 5-minute cache, and auto-refresh. Switching models via `/model` forces a refresh.
156
+ All queries use a 15-second timeout, a 5-minute cache, and auto-refresh. Switching models via `/model` forces a refresh, and `/usage-refresh` refreshes whichever probe currently owns the row.
@@ -54,31 +54,48 @@ export function formatCodexUsageStatusline(
54
54
  const snapshot = selectSnapshotForModel(report, model);
55
55
  if (!snapshot) return "usage unavailable";
56
56
 
57
- const weekly = selectWeeklyWindow(snapshot);
58
- if (weekly) return formatStatuslineWindow(snapshot, weekly);
59
-
60
- // A model-specific bucket may omit the long window; fall back to the account-wide one.
57
+ // A model-specific bucket may omit a window; fall back to the account-wide one.
61
58
  const codexSnapshot = report.snapshots.find(isPrimaryCodexSnapshot);
62
- if (codexSnapshot && codexSnapshot !== snapshot) {
63
- const fallbackWeekly = selectWeeklyWindow(codexSnapshot);
64
- if (fallbackWeekly) return formatStatuslineWindow(codexSnapshot, fallbackWeekly);
65
- }
59
+ const fallbackSnapshot = codexSnapshot !== snapshot ? codexSnapshot : undefined;
60
+ const short = selectShortWindow(snapshot) ?? selectShortWindow(fallbackSnapshot);
61
+ const weekly = selectWeeklyWindow(snapshot) ?? selectWeeklyWindow(fallbackSnapshot);
62
+
63
+ const ownsWindow = Boolean(selectShortWindow(snapshot) ?? selectWeeklyWindow(snapshot));
64
+ const labelSnapshot = ownsWindow ? snapshot : (fallbackSnapshot ?? snapshot);
65
+ const parts = [formatStatuslinePrefix(labelSnapshot)];
66
+
67
+ if (short) parts.push(formatStatuslineWindow(short, "5h", true));
68
+ if (weekly) parts.push(formatStatuslineWindow(weekly, "weekly", false));
69
+ if (!short && !weekly) parts.push("limits unavailable");
66
70
 
67
- return `${formatStatuslinePrefix(snapshot)} weekly unavailable`;
71
+ return parts.join(" ");
68
72
  }
69
73
 
70
74
  function formatStatuslineWindow(
71
- snapshot: NormalizedRateLimitSnapshot,
72
75
  window: NormalizedRateLimitWindow,
76
+ fallback: "5h" | "weekly",
77
+ withReset: boolean,
73
78
  ): string {
74
- return `${formatStatuslinePrefix(snapshot)} ${formatRemainingPercent(window)} ${formatWindowLabel(window, "weekly", true)}`;
79
+ const label = formatWindowLabel(window, fallback, true);
80
+ const reset = withReset && window.resetsAt ? ` (${formatCompactReset(window.resetsAt)})` : "";
81
+ return `${formatRemainingPercent(window)} ${label}${reset}`;
82
+ }
83
+
84
+ // The short (rolling) window is the one shorter than a day; accounts that only
85
+ // report a single long window have no short window at all.
86
+ function selectShortWindow(
87
+ snapshot: NormalizedRateLimitSnapshot | undefined,
88
+ ): NormalizedRateLimitWindow | undefined {
89
+ if (!snapshot?.primary) return undefined;
90
+ return isLongWindow(snapshot.primary) ? undefined : snapshot.primary;
75
91
  }
76
92
 
77
93
  // Prefer the secondary (long) window, but some accounts only report a single
78
94
  // long window as `primary` — treat any window of a day or more as the weekly one.
79
95
  function selectWeeklyWindow(
80
- snapshot: NormalizedRateLimitSnapshot,
96
+ snapshot: NormalizedRateLimitSnapshot | undefined,
81
97
  ): NormalizedRateLimitWindow | undefined {
98
+ if (!snapshot) return undefined;
82
99
  if (snapshot.secondary) return snapshot.secondary;
83
100
  if (snapshot.primary && isLongWindow(snapshot.primary)) return snapshot.primary;
84
101
  return undefined;
@@ -256,6 +273,20 @@ function formatReset(epochSeconds: number): string {
256
273
  return `${time} on ${day} ${month}`;
257
274
  }
258
275
 
276
+ // Compact reset stamp for the footer usage row: `14:30`, or `14:30 12 Feb`
277
+ // when the window rolls over on a later day.
278
+ function formatCompactReset(epochSeconds: number): string {
279
+ const reset = new Date(epochSeconds * 1000);
280
+ if (Number.isNaN(reset.getTime())) return "unknown";
281
+
282
+ const time = `${reset.getHours().toString().padStart(2, "0")}:${reset
283
+ .getMinutes()
284
+ .toString()
285
+ .padStart(2, "0")}`;
286
+ if (reset.toDateString() === new Date().toDateString()) return time;
287
+ return `${time} ${reset.getDate()} ${reset.toLocaleDateString(undefined, { month: "short" })}`;
288
+ }
289
+
259
290
  export function formatQueryErrors(errors: UsageQueryError[]): string {
260
291
  const lines = ["Unable to read Codex usage."];
261
292
  for (const error of errors) {
@@ -5,6 +5,13 @@ import type {
5
5
  } from "@earendil-works/pi-coding-agent";
6
6
  import { formatCodexUsageStatusline, formatQueryErrors, showReport } from "./format.ts";
7
7
  import { isOpenAICodexModel, isStaleExtensionContextError, queryUsage } from "./query.ts";
8
+ import {
9
+ DEEPSEEK_PROVIDER_ID,
10
+ formatDeepSeekBalance,
11
+ isDeepSeekModel,
12
+ queryDeepSeekBalance,
13
+ type DeepSeekBalance,
14
+ } from "../deepseek-balance.ts";
8
15
  import {
9
16
  formatTokenSwitchBalance,
10
17
  isTokenSwitchModel,
@@ -28,6 +35,11 @@ interface CommandArgumentCompletion {
28
35
  description?: string;
29
36
  }
30
37
 
38
+ /** Providers whose statusline is an account balance rather than a rate-limit window. */
39
+ function isBalanceProviderModel(model: Pick<CodexUsageModel, "provider"> | undefined): boolean {
40
+ return isTokenSwitchModel(model) || isDeepSeekModel(model);
41
+ }
42
+
31
43
  /** Provider-specific hooks driving the shared usage-statusline refresh pipeline. */
32
44
  interface UsageProbe<T> {
33
45
  readCache: () => { createdAt: number; value: T } | undefined;
@@ -59,6 +71,7 @@ export default function registerCodexUsage(
59
71
  ) {
60
72
  let cache: CachedReport | undefined;
61
73
  let tokenSwitchCache: { createdAt: number; balance: number } | undefined;
74
+ let deepSeekCache: { createdAt: number; balance: DeepSeekBalance } | undefined;
62
75
  let statuslineClearTimer: ReturnType<typeof setTimeout> | undefined;
63
76
  let statuslineRefreshTimer: ReturnType<typeof setTimeout> | undefined;
64
77
  let statuslineRequestId = 0;
@@ -92,6 +105,19 @@ export default function registerCodexUsage(
92
105
  }
93
106
  };
94
107
 
108
+ // The DeepSeek balance rides on the credential Pi already holds for the
109
+ // provider, so a model switched in via /login works without extra setup.
110
+ const runDeepSeekQuery = async (ctx: ExtensionContext, timeoutMs: number) => {
111
+ const apiKey = await ctx.modelRegistry.getApiKeyForProvider(DEEPSEEK_PROVIDER_ID);
112
+ const controller = new AbortController();
113
+ activeQueries.add(controller);
114
+ try {
115
+ return await queryDeepSeekBalance({ apiKey, timeoutMs, signal: controller.signal });
116
+ } finally {
117
+ activeQueries.delete(controller);
118
+ }
119
+ };
120
+
95
121
  const clearStatuslineTimers = () => {
96
122
  if (statuslineClearTimer) clearTimeout(statuslineClearTimer);
97
123
  if (statuslineRefreshTimer) clearTimeout(statuslineRefreshTimer);
@@ -235,6 +261,23 @@ export default function registerCodexUsage(
235
261
  activeStatuslineContext = ctx;
236
262
  const selectedModel = model ?? ctx.model;
237
263
 
264
+ if (isDeepSeekModel(selectedModel)) {
265
+ await refreshUsageStatusline<DeepSeekBalance>(ctx, force, {
266
+ readCache: () =>
267
+ deepSeekCache
268
+ ? { createdAt: deepSeekCache.createdAt, value: deepSeekCache.balance }
269
+ : undefined,
270
+ writeCache: (balance) => {
271
+ deepSeekCache = { createdAt: Date.now(), balance };
272
+ },
273
+ query: () => runDeepSeekQuery(ctx, DEFAULT_TIMEOUT_MS),
274
+ render: formatDeepSeekBalance,
275
+ pendingLabel: "checking balance",
276
+ errorLabel: "balance error",
277
+ });
278
+ return;
279
+ }
280
+
238
281
  if (isTokenSwitchModel(selectedModel)) {
239
282
  await refreshUsageStatusline<number>(ctx, force, {
240
283
  readCache: () =>
@@ -284,9 +327,9 @@ export default function registerCodexUsage(
284
327
  return;
285
328
  }
286
329
 
287
- // On a Token Switch model the statusline belongs to the balance probe, so the
288
- // Codex report is shown without overwriting it.
289
- const useStatusline = options.value.statusline && !isTokenSwitchModel(ctx.model);
330
+ // On a balance provider the statusline belongs to that probe, so the Codex
331
+ // report is shown without overwriting it.
332
+ const useStatusline = options.value.statusline && !isBalanceProviderModel(ctx.model);
290
333
 
291
334
  const cached = cache && Date.now() - cache.createdAt < CACHE_TTL_MS ? cache : undefined;
292
335
  if (cached && !options.value.refresh) {
@@ -338,7 +381,7 @@ export default function registerCodexUsage(
338
381
  description: "Refresh the model usage or balance shown in the footer now",
339
382
  handler: async (_args, ctx) => {
340
383
  // Refresh whichever probe owns the statusline for the active provider.
341
- if (isTokenSwitchModel(ctx.model)) {
384
+ if (isBalanceProviderModel(ctx.model)) {
342
385
  await refreshCurrentUsageStatusline(ctx, true).catch(rethrowUnlessStaleContextError(ctx));
343
386
  return;
344
387
  }
@@ -353,6 +396,7 @@ export default function registerCodexUsage(
353
396
  clearStatuslineTimers();
354
397
  cache = undefined;
355
398
  tokenSwitchCache = undefined;
399
+ deepSeekCache = undefined;
356
400
  activeStatuslineContext = undefined;
357
401
  sessionActive = true;
358
402
  void refreshCurrentUsageStatusline(ctx, false, ctx.model).catch(
@@ -0,0 +1,128 @@
1
+ export const DEEPSEEK_PROVIDER_ID = "deepseek";
2
+ const DEEPSEEK_BALANCE_URL = "https://api.deepseek.com/user/balance";
3
+ const MAX_RESPONSE_BODY_BYTES = 64 * 1024;
4
+
5
+ // DeepSeek reports one entry per currency; CNY is the default top-up currency,
6
+ // so an account holding both reads out its CNY balance.
7
+ const PREFERRED_CURRENCY = "CNY";
8
+ const CURRENCY_SYMBOLS: Record<string, string> = { CNY: "¥", USD: "$" };
9
+
10
+ export type DeepSeekModel = { provider: string };
11
+
12
+ export type DeepSeekBalance = { amount: number; currency: string };
13
+
14
+ type QueryDeepSeekBalanceOptions = {
15
+ apiKey?: string;
16
+ fetchImpl?: typeof fetch;
17
+ timeoutMs: number;
18
+ signal?: AbortSignal;
19
+ };
20
+
21
+ export function isDeepSeekModel(model: Pick<DeepSeekModel, "provider"> | undefined): boolean {
22
+ return model?.provider === DEEPSEEK_PROVIDER_ID;
23
+ }
24
+
25
+ export function parseDeepSeekBalance(payload: unknown): DeepSeekBalance {
26
+ const infos = readBalanceInfos(payload);
27
+ return infos.find((info) => info.currency === PREFERRED_CURRENCY) ?? infos[0]!;
28
+ }
29
+
30
+ export function formatDeepSeekBalance(balance: DeepSeekBalance): string {
31
+ const symbol = CURRENCY_SYMBOLS[balance.currency];
32
+ return symbol
33
+ ? `deepseek ${symbol}${balance.amount.toFixed(2)}`
34
+ : `deepseek ${balance.amount.toFixed(2)} ${balance.currency}`;
35
+ }
36
+
37
+ export async function queryDeepSeekBalance({
38
+ apiKey,
39
+ fetchImpl = fetch,
40
+ timeoutMs,
41
+ signal,
42
+ }: QueryDeepSeekBalanceOptions): Promise<DeepSeekBalance> {
43
+ if (!apiKey?.trim()) {
44
+ throw new Error(
45
+ "No DeepSeek API key is available. Use a DeepSeek model or run /login for DeepSeek.",
46
+ );
47
+ }
48
+
49
+ const controller = new AbortController();
50
+ const abortFromExternal = () => controller.abort();
51
+ if (signal?.aborted) controller.abort();
52
+ else signal?.addEventListener("abort", abortFromExternal, { once: true });
53
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
54
+
55
+ try {
56
+ const response = await fetchImpl(DEEPSEEK_BALANCE_URL, {
57
+ method: "GET",
58
+ headers: {
59
+ Accept: "application/json",
60
+ Authorization: `Bearer ${apiKey}`,
61
+ },
62
+ redirect: "error",
63
+ signal: controller.signal,
64
+ });
65
+ if (!response.ok) {
66
+ throw new Error(`DeepSeek balance endpoint returned HTTP ${response.status}.`);
67
+ }
68
+
69
+ const text = await response.text();
70
+ if (Buffer.byteLength(text, "utf8") > MAX_RESPONSE_BODY_BYTES) {
71
+ throw new Error("DeepSeek balance response exceeded the size limit.");
72
+ }
73
+ let payload: unknown;
74
+ try {
75
+ payload = JSON.parse(text) as unknown;
76
+ } catch {
77
+ throw new Error("DeepSeek balance response was not valid JSON.");
78
+ }
79
+ return parseDeepSeekBalance(payload);
80
+ } catch (error) {
81
+ if (controller.signal.aborted) {
82
+ throw new Error(
83
+ signal?.aborted
84
+ ? "DeepSeek balance query was cancelled."
85
+ : `Timed out after ${Math.round(timeoutMs / 1000)}s while fetching the DeepSeek balance.`,
86
+ );
87
+ }
88
+ throw error;
89
+ } finally {
90
+ clearTimeout(timeout);
91
+ signal?.removeEventListener("abort", abortFromExternal);
92
+ }
93
+ }
94
+
95
+ function readBalanceInfos(payload: unknown): DeepSeekBalance[] {
96
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
97
+ throw new Error("DeepSeek balance response did not contain balance_infos.");
98
+ }
99
+ const infos = (payload as Record<string, unknown>).balance_infos;
100
+ if (!Array.isArray(infos) || infos.length === 0) {
101
+ throw new Error("DeepSeek balance response did not contain balance_infos.");
102
+ }
103
+ return infos.map(parseBalanceInfo);
104
+ }
105
+
106
+ function parseBalanceInfo(entry: unknown, index: number): DeepSeekBalance {
107
+ const label = `balance_infos[${index}]`;
108
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
109
+ throw new Error(`DeepSeek ${label} was not an object.`);
110
+ }
111
+
112
+ const record = entry as Record<string, unknown>;
113
+ const currency = record.currency;
114
+ if (typeof currency !== "string" || !currency.trim()) {
115
+ throw new Error(`DeepSeek ${label}.currency was not a currency code.`);
116
+ }
117
+
118
+ const total = record.total_balance;
119
+ if (typeof total !== "string" || !total.trim()) {
120
+ throw new Error(`DeepSeek ${label}.total_balance was not a decimal string.`);
121
+ }
122
+ const amount = Number(total);
123
+ if (!Number.isFinite(amount)) {
124
+ throw new Error(`DeepSeek ${label}.total_balance was not a finite number.`);
125
+ }
126
+
127
+ return { amount, currency: currency.trim().toUpperCase() };
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rinco-pi-hud",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "A dynamic HUD footer for Pi, ported from Sakura Zentui Footer.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -22,6 +22,7 @@
22
22
  "files": [
23
23
  "extensions",
24
24
  "docs",
25
+ "assets/readme/*.svg",
25
26
  "README.md",
26
27
  "LICENSE",
27
28
  "NOTICE"