pi-bar 0.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/LICENSE +21 -0
- package/README.md +82 -0
- package/assets/screenshot.png +0 -0
- package/extensions/status-footer.ts +211 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jenny Yu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# pi-bar
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/pi-bar)
|
|
4
|
+
[](https://www.npmjs.com/package/pi-bar)
|
|
5
|
+
|
|
6
|
+
**Never accidentally run Opus on a typo again.** pi-bar puts the active model, thinking level, and context usage in pi's footer so you catch wrong-model / wrong-reasoning / context-pressure problems before they cost you.
|
|
7
|
+
|
|
8
|
+
```text
|
|
9
|
+
◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+
> Renamed from `trifecta-footer` at 0.3.0. If you previously installed `trifecta-footer`, run `pi remove npm:trifecta-footer && pi install npm:pi-bar`.
|
|
15
|
+
|
|
16
|
+
## Why use it?
|
|
17
|
+
|
|
18
|
+
- **See the active model at a glance** — catch accidental model switches before an expensive or sensitive task starts.
|
|
19
|
+
- **Track thinking level in place** — immediately notice when you are using the wrong reasoning setting.
|
|
20
|
+
- **Watch context pressure early** — context usage turns green, yellow, then red as you approach the limit.
|
|
21
|
+
|
|
22
|
+
pi-bar is intentionally tiny: one small extension, no broad behavior changes, and no commands to learn. It replaces pi's built-in footer with a compact model / thinking / context status line that refreshes when the model changes, thinking level changes, and after assistant turns.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pi install npm:pi-bar
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
If pi is already running after install, reload resources:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
/reload
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Customization
|
|
37
|
+
|
|
38
|
+
pi-bar works out of the box, but you can tune it with environment variables before launching pi.
|
|
39
|
+
|
|
40
|
+
### Show or hide segments
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
PI_BAR_SHOW=model,thinking,context pi
|
|
44
|
+
PI_BAR_SHOW=model,context pi
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Allowed segments are `model`, `thinking`, and `context`.
|
|
48
|
+
|
|
49
|
+
### Change context thresholds
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
PI_BAR_THRESHOLDS=60,85 pi
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The first number is the warning/yellow threshold. The second number is the danger/red threshold. Defaults are `70,90`.
|
|
56
|
+
|
|
57
|
+
### Change icon style
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
PI_BAR_ICONS=plain pi
|
|
61
|
+
PI_BAR_ICONS=emoji pi
|
|
62
|
+
PI_BAR_ICONS=nerdfont pi
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Supported presets:
|
|
66
|
+
|
|
67
|
+
- `unicode` — default, e.g. `◈ gpt-5.5 ❯ ✦ think:medium ❯ ◷ 42.7% / 272k`
|
|
68
|
+
- `plain` — maximum compatibility, e.g. `model:gpt-5.5 | think:medium | ctx:42.7% / 272k`
|
|
69
|
+
- `emoji` — colorful emoji markers, e.g. `🤖 gpt-5.5 › ✨ think:medium › 🧭 42.7% / 272k`
|
|
70
|
+
- `nerdfont` — icon-font glyphs for terminals configured with a Nerd Font, e.g. ` gpt-5.5 think:medium 42.7% / 272k`
|
|
71
|
+
|
|
72
|
+
## Compatibility
|
|
73
|
+
|
|
74
|
+
The default `unicode` preset uses common Unicode symbols that render well in most modern terminals. If your terminal shows boxes or misaligned glyphs, use:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
PI_BAR_ICONS=plain pi
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Security note
|
|
81
|
+
|
|
82
|
+
Pi extensions run with your local user permissions. Review any pi package source before installing it.
|
|
Binary file
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-bar — footer/statusline extension.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the built-in footer with three left-aligned segments:
|
|
5
|
+
* ◈ <model name> ❯ ✦ think:<level> ❯ ◷ <context% / window>
|
|
6
|
+
*
|
|
7
|
+
* Examples:
|
|
8
|
+
* ◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M
|
|
9
|
+
*
|
|
10
|
+
* Re-renders on model change, thinking-level change, and after each
|
|
11
|
+
* assistant turn so context usage stays current.
|
|
12
|
+
*
|
|
13
|
+
* Configuration env vars:
|
|
14
|
+
* PI_BAR_SHOW=model,thinking,context
|
|
15
|
+
* PI_BAR_THRESHOLDS=70,90
|
|
16
|
+
* PI_BAR_ICONS=unicode|plain|emoji|nerdfont
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
21
|
+
|
|
22
|
+
type SegmentName = "model" | "thinking" | "context";
|
|
23
|
+
type IconPreset = "unicode" | "plain" | "emoji" | "nerdfont";
|
|
24
|
+
|
|
25
|
+
const DEFAULT_SEGMENTS: SegmentName[] = ["model", "thinking", "context"];
|
|
26
|
+
const DEFAULT_WARNING_THRESHOLD = 70;
|
|
27
|
+
const DEFAULT_ERROR_THRESHOLD = 90;
|
|
28
|
+
|
|
29
|
+
const ICON_PRESETS: Record<
|
|
30
|
+
IconPreset,
|
|
31
|
+
{ model: string; thinking: string; context: string; separator: string }
|
|
32
|
+
> = {
|
|
33
|
+
unicode: { model: "◈", thinking: "✦", context: "◷", separator: "❯" },
|
|
34
|
+
plain: { model: "model", thinking: "think", context: "ctx", separator: "|" },
|
|
35
|
+
emoji: { model: "🤖", thinking: "✨", context: "🧭", separator: "›" },
|
|
36
|
+
nerdfont: { model: "", thinking: "", context: "", separator: "" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function formatTokens(n: number): string {
|
|
40
|
+
if (n >= 1_000_000) {
|
|
41
|
+
const value = n / 1_000_000;
|
|
42
|
+
return value >= 10 ? `${Math.round(value)}M` : `${value.toFixed(1)}M`;
|
|
43
|
+
}
|
|
44
|
+
if (n >= 1_000) {
|
|
45
|
+
const value = n / 1_000;
|
|
46
|
+
return value >= 10 ? `${Math.round(value)}k` : `${value.toFixed(1)}k`;
|
|
47
|
+
}
|
|
48
|
+
return `${n}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function formatModelName(id: string | undefined): string {
|
|
52
|
+
if (!id) return "no-model";
|
|
53
|
+
const base = id.includes("/") ? (id.split("/").pop() ?? id) : id;
|
|
54
|
+
return base.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function thinkingColor(level: string): string {
|
|
58
|
+
switch (level) {
|
|
59
|
+
case "off":
|
|
60
|
+
return "thinkingOff";
|
|
61
|
+
case "minimal":
|
|
62
|
+
case "min":
|
|
63
|
+
return "thinkingMinimal";
|
|
64
|
+
case "low":
|
|
65
|
+
return "thinkingLow";
|
|
66
|
+
case "medium":
|
|
67
|
+
case "med":
|
|
68
|
+
return "thinkingMedium";
|
|
69
|
+
case "high":
|
|
70
|
+
return "thinkingHigh";
|
|
71
|
+
case "xhigh":
|
|
72
|
+
case "extra-high":
|
|
73
|
+
return "thinkingXhigh";
|
|
74
|
+
default:
|
|
75
|
+
return "thinkingText";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function contextColor(
|
|
80
|
+
percent: number | null | undefined,
|
|
81
|
+
warningThreshold: number,
|
|
82
|
+
errorThreshold: number,
|
|
83
|
+
): string {
|
|
84
|
+
if (percent === null || percent === undefined) return "muted";
|
|
85
|
+
if (percent >= errorThreshold) return "error";
|
|
86
|
+
if (percent >= warningThreshold) return "warning";
|
|
87
|
+
return "success";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseSegments(): SegmentName[] {
|
|
91
|
+
const raw = process.env.PI_BAR_SHOW;
|
|
92
|
+
if (!raw) return DEFAULT_SEGMENTS;
|
|
93
|
+
|
|
94
|
+
const requested = raw
|
|
95
|
+
.split(",")
|
|
96
|
+
.map((segment) => segment.trim().toLowerCase())
|
|
97
|
+
.filter((segment): segment is SegmentName =>
|
|
98
|
+
["model", "thinking", "context"].includes(segment),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return requested.length > 0 ? requested : DEFAULT_SEGMENTS;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseThresholds(): { warningThreshold: number; errorThreshold: number } {
|
|
105
|
+
const raw = process.env.PI_BAR_THRESHOLDS;
|
|
106
|
+
if (!raw) {
|
|
107
|
+
return {
|
|
108
|
+
warningThreshold: DEFAULT_WARNING_THRESHOLD,
|
|
109
|
+
errorThreshold: DEFAULT_ERROR_THRESHOLD,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const [warning, error] = raw
|
|
114
|
+
.split(",")
|
|
115
|
+
.map((value) => Number.parseFloat(value.trim()));
|
|
116
|
+
|
|
117
|
+
if (
|
|
118
|
+
Number.isFinite(warning) &&
|
|
119
|
+
Number.isFinite(error) &&
|
|
120
|
+
warning >= 0 &&
|
|
121
|
+
error > warning
|
|
122
|
+
) {
|
|
123
|
+
return { warningThreshold: warning, errorThreshold: error };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
warningThreshold: DEFAULT_WARNING_THRESHOLD,
|
|
128
|
+
errorThreshold: DEFAULT_ERROR_THRESHOLD,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseIconPreset(): IconPreset {
|
|
133
|
+
const raw = process.env.PI_BAR_ICONS?.trim().toLowerCase();
|
|
134
|
+
if (raw === "plain" || raw === "emoji" || raw === "nerdfont") return raw;
|
|
135
|
+
return "unicode";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function prefixed(icon: string, text: string, preset: IconPreset): string {
|
|
139
|
+
if (preset === "plain") return `${icon}:${text}`;
|
|
140
|
+
return `${icon} ${text}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export default function (pi: ExtensionAPI) {
|
|
144
|
+
let requestRender: (() => void) | undefined;
|
|
145
|
+
const refresh = () => requestRender?.();
|
|
146
|
+
|
|
147
|
+
pi.on("model_select", async () => refresh());
|
|
148
|
+
pi.on("thinking_level_select", async () => refresh());
|
|
149
|
+
pi.on("turn_end", async () => refresh());
|
|
150
|
+
pi.on("message_end", async () => refresh());
|
|
151
|
+
|
|
152
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
153
|
+
if (!ctx.hasUI) return;
|
|
154
|
+
|
|
155
|
+
const visibleSegments = parseSegments();
|
|
156
|
+
const iconPreset = parseIconPreset();
|
|
157
|
+
const icons = ICON_PRESETS[iconPreset];
|
|
158
|
+
const { warningThreshold, errorThreshold } = parseThresholds();
|
|
159
|
+
|
|
160
|
+
ctx.ui.setFooter((tui, theme) => {
|
|
161
|
+
requestRender = () => tui.requestRender();
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
dispose() {
|
|
165
|
+
requestRender = undefined;
|
|
166
|
+
},
|
|
167
|
+
invalidate() {},
|
|
168
|
+
render(width: number): string[] {
|
|
169
|
+
const modelName = formatModelName(ctx.model?.id);
|
|
170
|
+
const thinkingLevel = String(pi.getThinkingLevel());
|
|
171
|
+
const usage = ctx.getContextUsage();
|
|
172
|
+
const contextSegmentColor = contextColor(
|
|
173
|
+
usage?.percent,
|
|
174
|
+
warningThreshold,
|
|
175
|
+
errorThreshold,
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const contextText = usage
|
|
179
|
+
? `${usage.percent !== null ? `${usage.percent.toFixed(1)}%` : "—%"} / ${formatTokens(usage.contextWindow)}`
|
|
180
|
+
: "—";
|
|
181
|
+
|
|
182
|
+
const segmentRenderers: Record<SegmentName, string> = {
|
|
183
|
+
model: theme.fg(
|
|
184
|
+
"accent",
|
|
185
|
+
prefixed(icons.model, modelName, iconPreset),
|
|
186
|
+
),
|
|
187
|
+
thinking: theme.fg(
|
|
188
|
+
thinkingColor(thinkingLevel),
|
|
189
|
+
prefixed(icons.thinking, `think:${thinkingLevel}`, iconPreset),
|
|
190
|
+
),
|
|
191
|
+
context: theme.fg(
|
|
192
|
+
contextSegmentColor,
|
|
193
|
+
prefixed(icons.context, contextText, iconPreset),
|
|
194
|
+
),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const separator = ` ${theme.fg("dim", icons.separator)} `;
|
|
198
|
+
const line = visibleSegments
|
|
199
|
+
.map((segment) => segmentRenderers[segment])
|
|
200
|
+
.join(separator);
|
|
201
|
+
|
|
202
|
+
return [truncateToWidth(line, width)];
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
209
|
+
if (ctx.hasUI) ctx.ui.setFooter(undefined);
|
|
210
|
+
});
|
|
211
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-bar",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Never accidentally run Opus on a typo. Pi footer/statusline showing active model, thinking level, and context usage.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi-extension",
|
|
9
|
+
"pi",
|
|
10
|
+
"pi-bar",
|
|
11
|
+
"footer",
|
|
12
|
+
"statusline",
|
|
13
|
+
"model",
|
|
14
|
+
"thinking",
|
|
15
|
+
"context",
|
|
16
|
+
"context-window"
|
|
17
|
+
],
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"files": [
|
|
20
|
+
"assets",
|
|
21
|
+
"extensions",
|
|
22
|
+
"README.md",
|
|
23
|
+
"LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
27
|
+
"@earendil-works/pi-tui": "*"
|
|
28
|
+
},
|
|
29
|
+
"pi": {
|
|
30
|
+
"extensions": [
|
|
31
|
+
"./extensions/status-footer.ts"
|
|
32
|
+
],
|
|
33
|
+
"image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.0/assets/screenshot.png"
|
|
34
|
+
}
|
|
35
|
+
}
|