pi-bar 0.3.41 → 0.3.43
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 +18 -3
- package/extensions/status-footer.ts +142 -32
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
**Never accidentally run Opus on a typo again.** pi-bar keeps your model, thinking level, context pressure, a live progress update, and any extension statuses visible in pi's footer.
|
|
7
7
|
|
|
8
8
|
```text
|
|
9
|
-
claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Reviewing package structure ❯
|
|
9
|
+
claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Reviewing package structure ❯ Plan active ❯ Queue: 2
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|

|
|
@@ -35,6 +35,19 @@ If pi is already running after install, reload resources:
|
|
|
35
35
|
/reload
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
## Automatic layout
|
|
39
|
+
|
|
40
|
+
The footer adapts to terminal width without extra configuration:
|
|
41
|
+
|
|
42
|
+
- **Model, thinking level, and context percentage take priority.**
|
|
43
|
+
- Separators have two spaces on each side by default, for a roomier display.
|
|
44
|
+
- Progress text shrinks first. If space is still tight, separator padding reduces to one space per side before compacting content. CWD then compacts from its preferred path to `parent/project`, then `project`; context window size is omitted when needed.
|
|
45
|
+
- Extension badges stay in their published order. Badges that cannot fit are hidden whole, with `+N` showing how many are behind the overflow. Open `/bar status` to inspect their text and visibility by key.
|
|
46
|
+
- On very narrow terminals, progress and CWD yield to core information. If even the core cannot fit, the model name is truncated first. At extreme widths, not every value or overflow count can remain visible.
|
|
47
|
+
- Widening the terminal restores the full display. Thinking and context retain their semantic colors throughout.
|
|
48
|
+
|
|
49
|
+
Existing segment visibility settings still apply; CWD remains opt-in.
|
|
50
|
+
|
|
38
51
|
## Customization
|
|
39
52
|
|
|
40
53
|
pi-bar works out of the box. Run `/bar` inside pi to choose which footer segments and extension statuses are shown:
|
|
@@ -99,7 +112,9 @@ Or in pi settings: `bar.progressModel`. Otherwise pi-bar picks a fast model you
|
|
|
99
112
|
|
|
100
113
|
### Configure extension statuses
|
|
101
114
|
|
|
102
|
-
Other pi extensions can publish small status badges. Pi-bar
|
|
115
|
+
Other pi extensions can publish small status badges. Pi-bar displays their text as written, without prepending internal keys: `setStatus("mcp", "MCP: 2/2 servers")` appears as `MCP: 2/2 servers`, not `mcp:MCP: 2/2 servers`. Extensions should publish self-describing text; pi-bar does not guess labels for bare values.
|
|
116
|
+
|
|
117
|
+
Pi-bar strips embedded terminal colors and control sequences, and separates each badge with the same `❯` divider used by other footer segments. Keys remain available for filtering and identification in `/bar`; selecting a status row shows its current sanitized text. Run `/bar` or `/bar status` inside pi to inspect statuses or pick which ones to show:
|
|
103
118
|
|
|
104
119
|
```text
|
|
105
120
|
/bar status
|
|
@@ -130,7 +145,7 @@ npm test
|
|
|
130
145
|
npm run check
|
|
131
146
|
```
|
|
132
147
|
|
|
133
|
-
Tests cover path formatting, terminal safety, width limits, and segment visibility
|
|
148
|
+
Tests cover path formatting, display-ready statuses, responsive layout, terminal safety, ANSI/Unicode width limits, resize behavior, and segment/status visibility persistence. Test configuration mirrors Pi's `pi-ai/compat` loader alias; runtime dependencies remain optional peers supplied by Pi.
|
|
134
149
|
|
|
135
150
|
## Security note
|
|
136
151
|
|
|
@@ -136,7 +136,6 @@ const DEFAULT_WARNING_THRESHOLD = 70;
|
|
|
136
136
|
const DEFAULT_ERROR_THRESHOLD = 90;
|
|
137
137
|
|
|
138
138
|
const SEGMENT_SEPARATOR = "❯";
|
|
139
|
-
const EXTENSION_STATUS_SEPARATOR = SEGMENT_SEPARATOR;
|
|
140
139
|
|
|
141
140
|
function formatTokens(n: number): string {
|
|
142
141
|
if (n >= 1_000_000) {
|
|
@@ -195,6 +194,111 @@ export function formatCwd(
|
|
|
195
194
|
return truncateToWidth(compact, maxWidth);
|
|
196
195
|
}
|
|
197
196
|
|
|
197
|
+
/** Preferred directory display, then parent/project, then project only. */
|
|
198
|
+
export function cwdVariants(
|
|
199
|
+
cwd: string,
|
|
200
|
+
home = homedir(),
|
|
201
|
+
maxWidth = DEFAULT_CWD_MAX_WIDTH,
|
|
202
|
+
paths = process.platform === "win32" ? win32 : posix,
|
|
203
|
+
): string[] {
|
|
204
|
+
const safeCwd = stripTerminalControls(cwd);
|
|
205
|
+
const leaf = paths.basename(safeCwd);
|
|
206
|
+
const parent = paths.basename(paths.dirname(safeCwd));
|
|
207
|
+
return [
|
|
208
|
+
formatCwd(cwd, home, maxWidth, paths),
|
|
209
|
+
...[parent && leaf ? `${parent}${paths.sep}${leaf}` : leaf, leaf]
|
|
210
|
+
.filter(Boolean)
|
|
211
|
+
.map((text) => truncateToWidth(stripTerminalControls(text), maxWidth, "…")),
|
|
212
|
+
].map(stripTerminalControls);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export type FooterSegment = {
|
|
216
|
+
name: SegmentName;
|
|
217
|
+
text: string;
|
|
218
|
+
/** Shorter, already styled representations, in preference order. */
|
|
219
|
+
alternatives?: readonly string[];
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/** Pure, column-aware layout. The divider may be styled but has no padding. Never split a status badge. */
|
|
223
|
+
export function layoutFooter(
|
|
224
|
+
segments: readonly FooterSegment[],
|
|
225
|
+
badges: readonly string[],
|
|
226
|
+
width: number,
|
|
227
|
+
divider = SEGMENT_SEPARATOR,
|
|
228
|
+
): string {
|
|
229
|
+
if (!Number.isFinite(width) || width <= 0) return "";
|
|
230
|
+
width = Math.floor(width);
|
|
231
|
+
let separator = ` ${divider} `;
|
|
232
|
+
const items = segments.map((segment) => ({ ...segment }));
|
|
233
|
+
const extensions = items.find((item) => item.name === "extensions");
|
|
234
|
+
let shownBadges = badges.length;
|
|
235
|
+
const updateBadges = () => {
|
|
236
|
+
if (!extensions) return;
|
|
237
|
+
const hidden = badges.length - shownBadges;
|
|
238
|
+
extensions.text = [...badges.slice(0, shownBadges), ...(hidden ? [`+${hidden}`] : [])].join(separator);
|
|
239
|
+
};
|
|
240
|
+
updateBadges();
|
|
241
|
+
const line = () => items.map((item) => item.text).filter(Boolean).join(separator);
|
|
242
|
+
const overflow = () => visibleWidth(line()) - width;
|
|
243
|
+
const progress = items.find((item) => item.name === "progress");
|
|
244
|
+
if (progress && overflow() > 0) {
|
|
245
|
+
const currentWidth = visibleWidth(progress.text);
|
|
246
|
+
// Keep a short activity fragment while trying the other compact forms.
|
|
247
|
+
const minimumWidth = Math.min(12, width);
|
|
248
|
+
const progressWidth = Math.min(currentWidth, Math.max(minimumWidth, currentWidth - overflow()));
|
|
249
|
+
progress.text = truncateToWidth(progress.text, progressWidth, "…");
|
|
250
|
+
}
|
|
251
|
+
// Keep the roomy default while progress alone can absorb width pressure.
|
|
252
|
+
// Tighten gaps before compacting paths/context or overflowing whole badges.
|
|
253
|
+
if (overflow() > 0) {
|
|
254
|
+
separator = ` ${divider} `;
|
|
255
|
+
updateBadges();
|
|
256
|
+
}
|
|
257
|
+
for (const name of ["cwd", "context", "thinking"] as const) {
|
|
258
|
+
const item = items.find((item) => item.name === name);
|
|
259
|
+
if (!item) continue;
|
|
260
|
+
for (const alternative of item.alternatives ?? []) {
|
|
261
|
+
if (overflow() <= 0) break;
|
|
262
|
+
if (visibleWidth(alternative) < visibleWidth(item.text)) item.text = alternative;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
while (extensions && shownBadges > 0 && overflow() > 0) {
|
|
266
|
+
shownBadges--;
|
|
267
|
+
updateBadges();
|
|
268
|
+
}
|
|
269
|
+
// On very narrow terminals, remove optional content before clipping identity.
|
|
270
|
+
for (const name of ["progress", "cwd", "extensions"] as const) {
|
|
271
|
+
if (overflow() <= 0) break;
|
|
272
|
+
const item = items.find((item) => item.name === name);
|
|
273
|
+
if (item) {
|
|
274
|
+
item.text = name === "cwd"
|
|
275
|
+
? truncateToWidth(item.text, Math.max(0, visibleWidth(item.text) - overflow()), "…")
|
|
276
|
+
: "";
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const model = items.find((item) => item.name === "model");
|
|
280
|
+
if (model && overflow() > 0) {
|
|
281
|
+
model.text = truncateToWidth(model.text, Math.max(0, visibleWidth(model.text) - overflow()), "…");
|
|
282
|
+
}
|
|
283
|
+
// Whole-badge overflow can free more space than needed. Use that space rather
|
|
284
|
+
// than leaving unnecessarily compact context, paths, or progress on screen.
|
|
285
|
+
for (const name of ["context", "thinking", "cwd", "progress"] as const) {
|
|
286
|
+
const item = items.find((item) => item.name === name);
|
|
287
|
+
const original = segments.find((segment) => segment.name === name);
|
|
288
|
+
if (!item?.text || !original) continue;
|
|
289
|
+
const budget = visibleWidth(item.text) - overflow();
|
|
290
|
+
if (name === "progress") {
|
|
291
|
+
item.text = truncateToWidth(original.text, Math.max(0, budget), "…");
|
|
292
|
+
} else {
|
|
293
|
+
const preferred = [original.text, ...(original.alternatives ?? [])]
|
|
294
|
+
.find((text) => visibleWidth(text) <= budget);
|
|
295
|
+
if (preferred !== undefined) item.text = preferred;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// A terminal narrower than thinking + context cannot show every core value.
|
|
299
|
+
return truncateToWidth(line(), width, "…");
|
|
300
|
+
}
|
|
301
|
+
|
|
198
302
|
function thinkingColor(level: string): ThemeColor {
|
|
199
303
|
switch (level) {
|
|
200
304
|
case "off":
|
|
@@ -1566,7 +1670,7 @@ function shouldShowStatus(key: string, filter: StatusFilter): boolean {
|
|
|
1566
1670
|
return !filter.hidden.has(key);
|
|
1567
1671
|
}
|
|
1568
1672
|
|
|
1569
|
-
function formatExtensionStatuses(
|
|
1673
|
+
export function formatExtensionStatuses(
|
|
1570
1674
|
statuses: ReadonlyMap<string, string>,
|
|
1571
1675
|
filter: StatusFilter,
|
|
1572
1676
|
seenStatusKeys: Set<string>,
|
|
@@ -1577,9 +1681,7 @@ function formatExtensionStatuses(
|
|
|
1577
1681
|
seenStatusKeys.add(key);
|
|
1578
1682
|
return shouldShowStatus(key, filter);
|
|
1579
1683
|
})
|
|
1580
|
-
.map(([
|
|
1581
|
-
`${stripTerminalControls(key)}:${stripTerminalControls(text)}`,
|
|
1582
|
-
);
|
|
1684
|
+
.map(([, text]) => stripTerminalControls(text));
|
|
1583
1685
|
|
|
1584
1686
|
return parts.length > 0 ? parts : null;
|
|
1585
1687
|
}
|
|
@@ -1699,6 +1801,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1699
1801
|
let statusFilter: StatusFilter = { mode: "all", hidden: new Set() };
|
|
1700
1802
|
let visibleSegments: SegmentName[] = readGlobalSegments() ?? DEFAULT_SEGMENTS;
|
|
1701
1803
|
const seenStatusKeys = new Set<string>();
|
|
1804
|
+
let currentStatuses: ReadonlyMap<string, string> = new Map();
|
|
1805
|
+
const statusDescription = (key: string) =>
|
|
1806
|
+
stripTerminalControls(currentStatuses.get(key) ?? "") || "No current status text";
|
|
1702
1807
|
const refresh = () => requestRender?.();
|
|
1703
1808
|
const progress = new FooterProgressEngine(refresh);
|
|
1704
1809
|
const restoreStatusFilter = (ctx: ExtensionContext) => {
|
|
@@ -1783,8 +1888,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1783
1888
|
},
|
|
1784
1889
|
...knownStatusKeys.map((key): SettingItem => ({
|
|
1785
1890
|
id: `status:${key}`,
|
|
1786
|
-
label: `Status: ${key}`,
|
|
1787
|
-
description:
|
|
1891
|
+
label: `Status: ${stripTerminalControls(key)}`,
|
|
1892
|
+
description: statusDescription(key),
|
|
1788
1893
|
currentValue: statusVisibility.get(key) ? "shown" : "hidden",
|
|
1789
1894
|
values: ["shown", "hidden"],
|
|
1790
1895
|
})),
|
|
@@ -1899,8 +2004,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
1899
2004
|
},
|
|
1900
2005
|
...knownStatusKeys.map((key): SettingItem => ({
|
|
1901
2006
|
id: key,
|
|
1902
|
-
label: key,
|
|
1903
|
-
description:
|
|
2007
|
+
label: stripTerminalControls(key),
|
|
2008
|
+
description: statusDescription(key),
|
|
1904
2009
|
currentValue: statusVisibility.get(key) ? "shown" : "hidden",
|
|
1905
2010
|
values: ["shown", "hidden"],
|
|
1906
2011
|
})),
|
|
@@ -2134,8 +2239,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
2134
2239
|
render(width: number): string[] {
|
|
2135
2240
|
const modelName = formatModelName(ctx.model?.id);
|
|
2136
2241
|
const thinkingLevel = String(pi.getThinkingLevel());
|
|
2242
|
+
currentStatuses = footerData?.getExtensionStatuses?.() ?? new Map();
|
|
2137
2243
|
const extensionStatusParts = formatExtensionStatuses(
|
|
2138
|
-
|
|
2244
|
+
currentStatuses,
|
|
2139
2245
|
statusFilter,
|
|
2140
2246
|
seenStatusKeys,
|
|
2141
2247
|
);
|
|
@@ -2151,29 +2257,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
2151
2257
|
: "—";
|
|
2152
2258
|
const progressText = progress.text();
|
|
2153
2259
|
|
|
2154
|
-
const
|
|
2155
|
-
?
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
:
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
:
|
|
2166
|
-
|
|
2167
|
-
|
|
2260
|
+
const directories = visibleSegments.includes("cwd")
|
|
2261
|
+
? cwdVariants(ctx.cwd, homedir(), cwdWidth).map((text) => theme.fg("accent", text))
|
|
2262
|
+
: [];
|
|
2263
|
+
const contextCompact = usage
|
|
2264
|
+
? usage.percent !== null ? `${usage.percent.toFixed(1)}%` : "—%"
|
|
2265
|
+
: "—";
|
|
2266
|
+
const shortThinking = thinkingLevel === "medium" ? "med" : thinkingLevel === "minimal" ? "min" : thinkingLevel;
|
|
2267
|
+
const renderers: Record<SegmentName, Omit<FooterSegment, "name">> = {
|
|
2268
|
+
model: { text: theme.fg("accent", stripTerminalControls(modelName)) },
|
|
2269
|
+
thinking: {
|
|
2270
|
+
text: theme.fg(thinkingColor(thinkingLevel), `think:${thinkingLevel}`),
|
|
2271
|
+
alternatives: [theme.fg(thinkingColor(thinkingLevel), `think:${shortThinking}`)],
|
|
2272
|
+
},
|
|
2273
|
+
context: {
|
|
2274
|
+
text: theme.fg(contextSegmentColor, contextText),
|
|
2275
|
+
alternatives: [theme.fg(contextSegmentColor, contextCompact)],
|
|
2276
|
+
},
|
|
2277
|
+
cwd: { text: directories[0] ?? "", alternatives: directories.slice(1) },
|
|
2278
|
+
progress: { text: progressText ? theme.fg("text", progressText) : "" },
|
|
2279
|
+
extensions: { text: "" },
|
|
2168
2280
|
};
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
.
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
return [truncateToWidth(line, width)];
|
|
2281
|
+
return [layoutFooter(
|
|
2282
|
+
visibleSegments.map((name) => ({ name, ...renderers[name] })),
|
|
2283
|
+
(extensionStatusParts ?? []).map((part) => theme.fg("text", part)),
|
|
2284
|
+
width,
|
|
2285
|
+
theme.fg("dim", SEGMENT_SEPARATOR),
|
|
2286
|
+
)];
|
|
2177
2287
|
},
|
|
2178
2288
|
};
|
|
2179
2289
|
});
|
package/package.json
CHANGED