rinco-pi-hud 1.2.0 → 1.3.1
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 +88 -126
- package/assets/readme/hero.svg +21 -0
- package/assets/readme/responsive.svg +36 -0
- package/docs/CONTRIBUTING.md +17 -9
- package/docs/README.zh-CN.md +88 -126
- package/docs/footer.md +7 -4
- package/extensions/hud/commands/settings.ts +2 -2
- package/extensions/hud/config/config-counts.ts +3 -0
- package/extensions/hud/footer/index.ts +2 -11
- package/extensions/hud/footer/render-labels.ts +4 -5
- package/extensions/hud/segments/mcp-status.ts +26 -21
- package/extensions/hud/state/index.ts +1 -0
- package/extensions/hud/telemetry/codex-usage/index.ts +48 -4
- package/extensions/hud/telemetry/deepseek-balance.ts +128 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,49 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
# Rinco Pi HUD
|
|
4
4
|
|
|
5
|
-
**A
|
|
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
|
[](https://github.com/earendil-works/pi) [](https://www.typescriptlang.org/) [](https://vitest.dev/) [](LICENSE)
|
|
8
8
|
|
|
9
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
27
|
+
Restart Pi afterwards, or run `/reload`. The footer is enabled by default and `/zentui` opens the settings UI.
|
|
39
28
|
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
+
## Commands
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
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,
|
|
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
|
|
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 configuration counts |
|
|
139
117
|
|
|
140
118
|
## Development
|
|
141
119
|
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
170
|
-
npm test
|
|
171
|
-
```
|
|
131
|
+
### README images
|
|
172
132
|
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
```bash
|
|
182
|
-
npm run verify
|
|
183
|
-
npm run pack:check
|
|
184
|
-
```
|
|
140
|
+
## Troubleshooting
|
|
185
141
|
|
|
186
|
-
|
|
142
|
+
<details>
|
|
143
|
+
<summary>The footer does not appear</summary>
|
|
187
144
|
|
|
188
|
-
|
|
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
|
-
|
|
148
|
+
<details>
|
|
149
|
+
<summary>The layout looks wrong in a narrow terminal</summary>
|
|
191
150
|
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
154
|
+
<details>
|
|
155
|
+
<summary>Colors look incorrect</summary>
|
|
197
156
|
|
|
198
|
-
The
|
|
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
|
-
|
|
160
|
+
<details>
|
|
161
|
+
<summary>Icons are missing or misaligned</summary>
|
|
201
162
|
|
|
202
|
-
|
|
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
|
-
|
|
166
|
+
## License
|
|
205
167
|
|
|
206
|
-
|
|
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>
|
package/docs/CONTRIBUTING.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
|
@@ -47,7 +50,8 @@ npm run pack:check
|
|
|
47
50
|
|
|
48
51
|
Tests live in `tests/`:
|
|
49
52
|
|
|
50
|
-
- `zentui-status.test.ts` — Config counts, MCP
|
|
53
|
+
- `zentui-status.test.ts` — Config counts, MCP configuration counts, and extension status helpers.
|
|
54
|
+
- `mcp-footer.test.ts` — MCP count rendering, segment controls, custom formats, and legacy third-party statuses.
|
|
51
55
|
- `codex-usage.test.ts` — Codex payload normalization and model-specific quota formatting.
|
|
52
56
|
- `footer-layout.test.ts` — Four-group categorized layout, segment wrapping, and narrow-width budget.
|
|
53
57
|
- `footer-groups.test.ts` — Semantic Project, Session, Activity, and Usage segment ordering.
|
|
@@ -56,6 +60,7 @@ Tests live in `tests/`:
|
|
|
56
60
|
- `model-switch-statusline.test.ts` — Model switch statusline integration.
|
|
57
61
|
- `model-usage-refresh.test.ts` — Model usage refresh integration.
|
|
58
62
|
- `token-switch-usage.test.ts` — Token Switch balance formatting.
|
|
63
|
+
- `deepseek-balance.test.ts` — DeepSeek balance parsing, formatting, and query safety.
|
|
59
64
|
|
|
60
65
|
When adding tests:
|
|
61
66
|
|
|
@@ -83,9 +88,7 @@ Follow the configured conventions:
|
|
|
83
88
|
- Use TypeScript ESM and explicit types.
|
|
84
89
|
- Use tab indentation, double quotes, and semicolons.
|
|
85
90
|
- Keep extension lifecycle handling clear and release timers and UI state during `session_shutdown`.
|
|
86
|
-
- Do not
|
|
87
|
-
|
|
88
|
-
Do not add ignore rules, skip tests, or weaken types merely to bypass a check.
|
|
91
|
+
- Do not add ignore rules, skip tests, or weaken types merely to bypass a check.
|
|
89
92
|
|
|
90
93
|
## Architecture notes
|
|
91
94
|
|
|
@@ -98,9 +101,14 @@ The HUD extension is organized into functional modules under `extensions/hud/`:
|
|
|
98
101
|
- **`segments/`** — State collectors: Git, runtime, MCP, skills, projects, etc.
|
|
99
102
|
- **`session/`** — Pi event registration, lifecycle generations, and live-context throttling.
|
|
100
103
|
- **`state/`** — Shared footer state, telemetry reducer, and project-refresh controller.
|
|
101
|
-
- **`telemetry/`** — Usage formatting,
|
|
104
|
+
- **`telemetry/`** — Usage formatting, provider balance probes, and Codex subscription client.
|
|
102
105
|
- **`ui/`** — Icons and terminal style utilities.
|
|
103
106
|
|
|
107
|
+
`assets/readme/` holds the README visuals. Both boards are generated, not drawn: `source/capture-footer.mts`
|
|
108
|
+
runs the real `installFooter` render path over a checkout plus a real Pi session transcript, and
|
|
109
|
+
`source/build-assets.mts` converts the ANSI rows into SVG. Regenerate them with `npx tsx` as described in
|
|
110
|
+
the README instead of editing the `.svg` files by hand.
|
|
111
|
+
|
|
104
112
|
Key conventions:
|
|
105
113
|
|
|
106
114
|
- Keep composition in `index.ts`, Pi event listeners in `session/event-handlers.ts`, and avoid file reads or subprocesses in `render()`.
|
package/docs/README.zh-CN.md
CHANGED
|
@@ -2,49 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
# Rinco Pi HUD
|
|
4
4
|
|
|
5
|
-
**为 [Pi](https://github.com/earendil-works/pi)
|
|
5
|
+
**为 [Pi](https://github.com/earendil-works/pi) 打造的实时状态脚注。** 项目、Git、会话、工具与费用状态分成四组,随你的操作即时更新。
|
|
6
6
|
|
|
7
7
|
[](https://github.com/earendil-works/pi) [](https://www.typescriptlang.org/) [](https://vitest.dev/) [](../LICENSE)
|
|
8
8
|
|
|
9
|
-
[
|
|
9
|
+
[安装](#安装) · [命令](#命令) · [显示什么](#显示什么) · [布局](#布局) · [开发](#开发)
|
|
10
10
|
|
|
11
11
|
[English](../README.md)
|
|
12
12
|
|
|
13
13
|
</div>
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
27
|
+
装完重启 Pi,或执行 `/reload`。脚注默认启用,`/zentui` 打开设置界面。
|
|
39
28
|
|
|
40
|
-
|
|
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
|
-
|
|
47
|
+
## 命令
|
|
60
48
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
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) |
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
```bash
|
|
182
|
-
npm run verify
|
|
183
|
-
npm run pack:check
|
|
184
|
-
```
|
|
140
|
+
## 常见问题
|
|
185
141
|
|
|
186
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
3. 使用 `pi list` 确认包已安装。
|
|
151
|
+
脚注会测量终端宽度,只在状态段边界换行,所以窗口越窄,每行容纳的状态段越少。可以设置 `footerFormat` 模板,或在 `/zentui` 中关闭低优先级状态段。
|
|
152
|
+
</details>
|
|
195
153
|
|
|
196
|
-
|
|
154
|
+
<details>
|
|
155
|
+
<summary>颜色显示异常</summary>
|
|
197
156
|
|
|
198
|
-
|
|
157
|
+
默认配色输出 `38;2;r;g;b` Truecolor 序列,不支持 Truecolor 的终端会近似处理或直接忽略。
|
|
158
|
+
</details>
|
|
199
159
|
|
|
200
|
-
|
|
160
|
+
<details>
|
|
161
|
+
<summary>图标缺失或错位</summary>
|
|
201
162
|
|
|
202
|
-
|
|
163
|
+
Unicode 与 Nerd Font 符号的显示效果因终端和字体而异。建议安装 Nerd Font,或在 `/zentui` 中切换到 ASCII 图标。
|
|
164
|
+
</details>
|
|
203
165
|
|
|
204
|
-
|
|
166
|
+
## 许可证
|
|
205
167
|
|
|
206
|
-
|
|
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
|
@@ -7,10 +7,12 @@ When `footerFormat` is empty, the HUD displays four semantic groups:
|
|
|
7
7
|
```text
|
|
8
8
|
Project · pi-sakura · main · 24.18
|
|
9
9
|
Session · gpt-5.3 · Thinking level: high · Turn 8
|
|
10
|
-
Activity · Agent idle · Skill 2/3 · MCP 2
|
|
10
|
+
Activity · Agent idle · Skill 2/3 · MCP 2
|
|
11
11
|
Usage · 35%/200k · ↑ 4.2k · ↓ 1.1k · $ 0.030
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
+
The MCP segment counts server entries in Pi's agent-directory `mcp.json` and the project's `.pi/mcp.json`. Same-name entries count once; disabled entries are included. Zero entries hide the segment. Counts refresh with the project state polling interval. This does not report connection state, validate individual server settings, or include servers registered dynamically by extensions. Missing, unreadable, malformed, or larger-than-1-MiB files contribute no entries. Legacy adapter footer statuses remain ordinary third-party statuses.
|
|
15
|
+
|
|
14
16
|
The category column is sized from visible text so every separator aligns. When a row exceeds the terminal width, complete status segments move to an indented continuation line. Only a single segment that is wider than the available content area is truncated. Third-party extension statuses appear on the Activity group in configured `left`, `middle`, then `right` order.
|
|
15
17
|
|
|
16
18
|
Control each default-layout row independently:
|
|
@@ -50,7 +52,7 @@ The command shape is `/zentui row <project|session|activity|usage> <enable|disab
|
|
|
50
52
|
| `activeAgents` | on | Activity | Active primary agent runs |
|
|
51
53
|
| `agentIdle` | on | Activity | `Agent idle` when no primary agent run is active |
|
|
52
54
|
| `skills` | on | Activity | Active/available skills count, e.g. `Skill 1/3` |
|
|
53
|
-
| `mcp` | on | Activity | MCP
|
|
55
|
+
| `mcp` | on | Activity | File-configured MCP servers, e.g. `MCP 2` |
|
|
54
56
|
| `context` | on | Usage | Context percentage, window size, and/or gauge |
|
|
55
57
|
| `inputTokens` | on | Usage | Cumulative input tokens |
|
|
56
58
|
| `outputTokens` | on | Usage | Cumulative output tokens |
|
|
@@ -119,7 +121,7 @@ Clear it:
|
|
|
119
121
|
| `$skills` | Active/available skills, e.g. `Skill 1/3` |
|
|
120
122
|
| `$active_skills` | Active skills count only |
|
|
121
123
|
| `$extensions` | Installed Pi packages count |
|
|
122
|
-
| `$mcp` | MCP
|
|
124
|
+
| `$mcp` | File-configured MCP server count |
|
|
123
125
|
| `$tool_counts` | Native tool completion counts |
|
|
124
126
|
| `$running_tools` | Recently running tools |
|
|
125
127
|
| `$active_agents` | Active primary agent runs |
|
|
@@ -151,5 +153,6 @@ The status line automatically switches quota display based on model provider:
|
|
|
151
153
|
|
|
152
154
|
- **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
155
|
- **token-switch**: Uses `TOKEN_SWITCH_API_KEY` environment variable to query billing subscription and usage. Shows available balance, e.g. `token-switch $750.00`.
|
|
156
|
+
- **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
157
|
|
|
155
|
-
|
|
158
|
+
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.
|
|
@@ -133,7 +133,7 @@ const footerSegmentSettingLabels: Record<FooterSegmentSettingId, string> = {
|
|
|
133
133
|
activeAgents: "Active agents",
|
|
134
134
|
agentIdle: "Agent idle status",
|
|
135
135
|
skills: "Skill activity",
|
|
136
|
-
mcp: "MCP
|
|
136
|
+
mcp: "MCP configured servers",
|
|
137
137
|
context: "Context usage",
|
|
138
138
|
inputTokens: "Input tokens",
|
|
139
139
|
outputTokens: "Output tokens",
|
|
@@ -173,7 +173,7 @@ const footerSegmentSettingDescriptions: Record<FooterSegmentSettingId, string> =
|
|
|
173
173
|
codexUsage: "Show OpenAI Codex subscription rate limits on the usage row when available.",
|
|
174
174
|
configCounts: "Show instruction-file and installed Pi package counts on the project row.",
|
|
175
175
|
skills: "Show active/available Pi skill counts on the activity row.",
|
|
176
|
-
mcp: "Show
|
|
176
|
+
mcp: "Show the configured server count from global and project mcp.json, including disabled entries.",
|
|
177
177
|
runningTools: "Show currently running native tools and their elapsed time.",
|
|
178
178
|
toolCounts: "Show cumulative completed native-tool counts.",
|
|
179
179
|
activeAgents: "Show active main agent runs on the activity row (not subagent count).",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { readFileSync, statSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, join } from "node:path";
|
|
4
|
+
import { countConfiguredMcpServers } from "../segments/mcp-status.js";
|
|
4
5
|
|
|
5
6
|
export interface ConfigCounts {
|
|
6
7
|
instructionFiles: {
|
|
@@ -9,6 +10,7 @@ export interface ConfigCounts {
|
|
|
9
10
|
total: number;
|
|
10
11
|
};
|
|
11
12
|
packages: number;
|
|
13
|
+
mcpServers: number;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export interface ConfigCountOptions {
|
|
@@ -82,6 +84,7 @@ export function countConfigEntries(cwd: string, options: ConfigCountOptions = {}
|
|
|
82
84
|
total: agentsMd + claudeMd,
|
|
83
85
|
},
|
|
84
86
|
packages: countPackages(agentDir),
|
|
87
|
+
mcpServers: countConfiguredMcpServers(cwd, agentDir),
|
|
85
88
|
};
|
|
86
89
|
}
|
|
87
90
|
|
|
@@ -31,7 +31,6 @@ import {
|
|
|
31
31
|
} from "../telemetry/format.js";
|
|
32
32
|
import { resolveRuntimeSymbol } from "../ui/icons.js";
|
|
33
33
|
import type { LiveContextOverride } from "../session/live-context.js";
|
|
34
|
-
import { parseMcpStatus } from "../segments/mcp-status.js";
|
|
35
34
|
import type { FooterState } from "../state/index.js";
|
|
36
35
|
import { renderStyleForSource } from "../ui/style.js";
|
|
37
36
|
|
|
@@ -74,7 +73,6 @@ export function installFooter(
|
|
|
74
73
|
const colorSource = config.colorSources.starship;
|
|
75
74
|
const iconMode = config.icons.mode;
|
|
76
75
|
const rawExtensionStatuses = footerData.getExtensionStatuses();
|
|
77
|
-
const mcpStatus = parseMcpStatus(rawExtensionStatuses.get("mcp"));
|
|
78
76
|
const { safeCwd, safeRuntime, safePackageVersion, safeCommit } = sanitizeFooterInputs(
|
|
79
77
|
ctx,
|
|
80
78
|
state,
|
|
@@ -166,7 +164,7 @@ export function installFooter(
|
|
|
166
164
|
runningToolsLabel,
|
|
167
165
|
activeAgentsLabel,
|
|
168
166
|
agentIdleLabel,
|
|
169
|
-
} = buildTelemetryLabels(ctx, state, config, theme, colorSource
|
|
167
|
+
} = buildTelemetryLabels(ctx, state, config, theme, colorSource);
|
|
170
168
|
const renderVariable = (name: string): string => {
|
|
171
169
|
const canonical = FOOTER_FORMAT_ALIASES[name] ?? name;
|
|
172
170
|
switch (canonical) {
|
|
@@ -439,14 +437,7 @@ export function installFooter(
|
|
|
439
437
|
formatTimeLabel(""),
|
|
440
438
|
)}`
|
|
441
439
|
: "";
|
|
442
|
-
const
|
|
443
|
-
const dedicatedMcpVisible = Boolean(
|
|
444
|
-
mcpStatus && (config.footerFormat ? formatNeedsMcp : config.footerSegments.mcp),
|
|
445
|
-
);
|
|
446
|
-
const extensionStatusSource = dedicatedMcpVisible
|
|
447
|
-
? new Map([...rawExtensionStatuses].filter(([key]) => key !== "mcp"))
|
|
448
|
-
: rawExtensionStatuses;
|
|
449
|
-
const extensionStatuses = collectExtensionStatusSegments(extensionStatusSource, config);
|
|
440
|
+
const extensionStatuses = collectExtensionStatusSegments(rawExtensionStatuses, config);
|
|
450
441
|
const renderExtensionStatus = (segment: ExtensionStatusSegment) =>
|
|
451
442
|
segment.colorMode === "original"
|
|
452
443
|
? segment.text
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
13
13
|
import type { ColorSource, PolishedTuiConfig } from "../config/config.js";
|
|
14
|
-
import type { McpStatus } from "../segments/mcp-status.js";
|
|
15
14
|
import { formatSkillCounts } from "../segments/skill-activity.js";
|
|
16
15
|
import type { FooterState } from "../state/index.js";
|
|
17
16
|
import { getTelemetryStats } from "../state/telemetry.js";
|
|
@@ -164,7 +163,6 @@ export function buildTelemetryLabels(
|
|
|
164
163
|
config: PolishedTuiConfig,
|
|
165
164
|
theme: ThemeLike,
|
|
166
165
|
colorSource: ColorSource,
|
|
167
|
-
mcpStatus: McpStatus | undefined,
|
|
168
166
|
): TelemetryLabels {
|
|
169
167
|
const statusStyle = (style: string, text: string) =>
|
|
170
168
|
text ? renderStyleForSource(theme, colorSource, style, text) : "";
|
|
@@ -267,9 +265,10 @@ export function buildTelemetryLabels(
|
|
|
267
265
|
: "";
|
|
268
266
|
const configCountsLabel = [instructionFilesLabel, extensionsLabel].filter(Boolean).join(" ");
|
|
269
267
|
|
|
270
|
-
const mcpLabel =
|
|
271
|
-
|
|
272
|
-
|
|
268
|
+
const mcpLabel =
|
|
269
|
+
state.configCounts.mcpServers > 0
|
|
270
|
+
? statusStyle(config.colors.gitStatus, `MCP ${state.configCounts.mcpServers}`)
|
|
271
|
+
: "";
|
|
273
272
|
|
|
274
273
|
const telemetryStats = getTelemetryStats(state.telemetry);
|
|
275
274
|
const toolCountsText = Object.entries(telemetryStats.completedToolCounts)
|
|
@@ -1,26 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
const MAX_CONFIG_BYTES = 1024 * 1024;
|
|
5
|
+
|
|
6
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (![connected, total, disabled].every(Number.isSafeInteger)) return undefined;
|
|
21
|
-
if (connected < 0 || total < 0 || connected > total || total > 10_000 || disabled > 10_000)
|
|
22
|
-
return undefined;
|
|
23
|
-
return { connected, total };
|
|
10
|
+
function readServerNames(path: string): string[] {
|
|
11
|
+
try {
|
|
12
|
+
const stats = statSync(path);
|
|
13
|
+
if (!stats.isFile() || stats.size > MAX_CONFIG_BYTES) return [];
|
|
14
|
+
const config: unknown = JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
+
if (!isRecord(config) || !isRecord(config.mcpServers)) return [];
|
|
16
|
+
return Object.entries(config.mcpServers)
|
|
17
|
+
.filter(([name, entry]) => /^[A-Za-z0-9_-]+$/.test(name) && isRecord(entry))
|
|
18
|
+
.map(([name]) => name);
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
24
22
|
}
|
|
25
23
|
|
|
26
|
-
|
|
24
|
+
/** Count file-configured server entries, including disabled ones, without resolving secrets
|
|
25
|
+
* or starting connections. Project entries override global entries with the same name.
|
|
26
|
+
* This is not a connection count or a validation of individual server settings. */
|
|
27
|
+
export function countConfiguredMcpServers(cwd: string, agentDir: string): number {
|
|
28
|
+
const globalNames = agentDir ? readServerNames(join(agentDir, "mcp.json")) : [];
|
|
29
|
+
const projectNames = readServerNames(join(cwd, ".pi", "mcp.json"));
|
|
30
|
+
return new Set([...globalNames, ...projectNames]).size;
|
|
31
|
+
}
|
|
@@ -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
|
|
288
|
-
//
|
|
289
|
-
const useStatusline = options.value.statusline && !
|
|
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 (
|
|
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.
|
|
3
|
+
"version": "1.3.1",
|
|
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"
|