pi-bar 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/extensions/status-footer.ts +141 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M ❯ ✧ plan:active
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-

|
|
13
13
|
|
|
14
14
|
## Why use it?
|
|
15
15
|
|
|
@@ -45,6 +45,22 @@ PI_BAR_SHOW=model,context pi
|
|
|
45
45
|
|
|
46
46
|
Allowed segments are `model`, `thinking`, `context`, and `extensions`. The `extensions` segment is hidden when no extension has set a status.
|
|
47
47
|
|
|
48
|
+
### Configure extension statuses
|
|
49
|
+
|
|
50
|
+
pi-bar shows statuses set by other extensions with `ctx.ui.setStatus()`.
|
|
51
|
+
Use `/bar status` inside pi to choose which status keys appear.
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
/bar status list
|
|
55
|
+
/bar status hide plan-mode
|
|
56
|
+
/bar status show plan-mode
|
|
57
|
+
/bar status only plan-mode,preset
|
|
58
|
+
/bar status none
|
|
59
|
+
/bar status all
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Status filter choices persist in the current session.
|
|
63
|
+
|
|
48
64
|
### Change context thresholds
|
|
49
65
|
|
|
50
66
|
```bash
|
|
@@ -16,11 +16,19 @@
|
|
|
16
16
|
* PI_BAR_ICONS=unicode|plain|emoji|nerdfont
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
21
21
|
|
|
22
22
|
type SegmentName = "model" | "thinking" | "context" | "extensions";
|
|
23
23
|
type IconPreset = "unicode" | "plain" | "emoji" | "nerdfont";
|
|
24
|
+
type StatusFilter =
|
|
25
|
+
| { mode: "all"; hidden: Set<string> }
|
|
26
|
+
| { mode: "only"; shown: Set<string> };
|
|
27
|
+
type SerializedStatusFilter =
|
|
28
|
+
| { mode: "all"; hidden: string[] }
|
|
29
|
+
| { mode: "only"; shown: string[] };
|
|
30
|
+
|
|
31
|
+
const STATUS_FILTER_ENTRY_TYPE = "pi-bar-status-filter";
|
|
24
32
|
|
|
25
33
|
const DEFAULT_SEGMENTS: SegmentName[] = [
|
|
26
34
|
"model",
|
|
@@ -175,24 +183,153 @@ function prefixed(icon: string, text: string, preset: IconPreset): string {
|
|
|
175
183
|
return `${icon} ${text}`;
|
|
176
184
|
}
|
|
177
185
|
|
|
178
|
-
function
|
|
186
|
+
function shouldShowStatus(key: string, filter: StatusFilter): boolean {
|
|
187
|
+
if (filter.mode === "only") return filter.shown.has(key);
|
|
188
|
+
return !filter.hidden.has(key);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function formatExtensionStatuses(
|
|
192
|
+
statuses: ReadonlyMap<string, string>,
|
|
193
|
+
filter: StatusFilter,
|
|
194
|
+
seenStatusKeys: Set<string>,
|
|
195
|
+
): string | null {
|
|
179
196
|
const parts = Array.from(statuses.entries())
|
|
180
197
|
.filter(([, text]) => text.trim().length > 0)
|
|
198
|
+
.filter(([key]) => {
|
|
199
|
+
seenStatusKeys.add(key);
|
|
200
|
+
return shouldShowStatus(key, filter);
|
|
201
|
+
})
|
|
181
202
|
.map(([key, text]) => `${key}:${text}`);
|
|
182
203
|
|
|
183
204
|
return parts.length > 0 ? parts.join(" ") : null;
|
|
184
205
|
}
|
|
185
206
|
|
|
207
|
+
function serializeStatusFilter(filter: StatusFilter): SerializedStatusFilter {
|
|
208
|
+
if (filter.mode === "only") {
|
|
209
|
+
return { mode: "only", shown: Array.from(filter.shown).sort() };
|
|
210
|
+
}
|
|
211
|
+
return { mode: "all", hidden: Array.from(filter.hidden).sort() };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function parseSerializedStatusFilter(value: unknown): StatusFilter | null {
|
|
215
|
+
if (!value || typeof value !== "object") return null;
|
|
216
|
+
const data = value as Partial<SerializedStatusFilter>;
|
|
217
|
+
|
|
218
|
+
if (data.mode === "only" && Array.isArray(data.shown)) {
|
|
219
|
+
return { mode: "only", shown: new Set(data.shown) };
|
|
220
|
+
}
|
|
221
|
+
if (data.mode === "all" && Array.isArray(data.hidden)) {
|
|
222
|
+
return { mode: "all", hidden: new Set(data.hidden) };
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function splitStatusKeys(raw: string): string[] {
|
|
228
|
+
return raw
|
|
229
|
+
.split(/[\s,]+/)
|
|
230
|
+
.map((key) => key.trim())
|
|
231
|
+
.filter(Boolean);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function describeStatusFilter(filter: StatusFilter): string {
|
|
235
|
+
if (filter.mode === "only") {
|
|
236
|
+
const shown = Array.from(filter.shown).sort();
|
|
237
|
+
return shown.length > 0 ? `showing only: ${shown.join(", ")}` : "showing none";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const hidden = Array.from(filter.hidden).sort();
|
|
241
|
+
return hidden.length > 0 ? `showing all except: ${hidden.join(", ")}` : "showing all";
|
|
242
|
+
}
|
|
243
|
+
|
|
186
244
|
export default function (pi: ExtensionAPI) {
|
|
187
245
|
let requestRender: (() => void) | undefined;
|
|
246
|
+
let statusFilter: StatusFilter = { mode: "all", hidden: new Set() };
|
|
247
|
+
const seenStatusKeys = new Set<string>();
|
|
188
248
|
const refresh = () => requestRender?.();
|
|
249
|
+
const restoreStatusFilter = (ctx: ExtensionContext) => {
|
|
250
|
+
let restoredFilter: StatusFilter | null = null;
|
|
251
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
252
|
+
if (entry.type === "custom" && entry.customType === STATUS_FILTER_ENTRY_TYPE) {
|
|
253
|
+
restoredFilter = parseSerializedStatusFilter(entry.data);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
statusFilter = restoredFilter ?? { mode: "all", hidden: new Set() };
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
pi.registerCommand("bar", {
|
|
260
|
+
description: "Configure pi-bar footer status segments",
|
|
261
|
+
handler: async (args, ctx) => {
|
|
262
|
+
const [section, action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
263
|
+
if (!section || section === "status" || section === "statuses") {
|
|
264
|
+
const keys = splitStatusKeys(rest.join(" "));
|
|
265
|
+
|
|
266
|
+
switch (action) {
|
|
267
|
+
case undefined:
|
|
268
|
+
case "list":
|
|
269
|
+
case "ls": {
|
|
270
|
+
const known = Array.from(seenStatusKeys).sort();
|
|
271
|
+
ctx.ui.notify(
|
|
272
|
+
`pi-bar statuses: ${describeStatusFilter(statusFilter)}${known.length > 0 ? `; known: ${known.join(", ")}` : "; known: none yet"}`,
|
|
273
|
+
"info",
|
|
274
|
+
);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
case "all":
|
|
278
|
+
statusFilter = { mode: "all", hidden: new Set() };
|
|
279
|
+
break;
|
|
280
|
+
case "none":
|
|
281
|
+
statusFilter = { mode: "only", shown: new Set() };
|
|
282
|
+
break;
|
|
283
|
+
case "only":
|
|
284
|
+
statusFilter = { mode: "only", shown: new Set(keys) };
|
|
285
|
+
break;
|
|
286
|
+
case "hide":
|
|
287
|
+
if (statusFilter.mode === "only") {
|
|
288
|
+
for (const key of keys) statusFilter.shown.delete(key);
|
|
289
|
+
} else {
|
|
290
|
+
for (const key of keys) statusFilter.hidden.add(key);
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
case "show":
|
|
294
|
+
if (statusFilter.mode === "only") {
|
|
295
|
+
for (const key of keys) statusFilter.shown.add(key);
|
|
296
|
+
} else {
|
|
297
|
+
for (const key of keys) statusFilter.hidden.delete(key);
|
|
298
|
+
}
|
|
299
|
+
break;
|
|
300
|
+
default:
|
|
301
|
+
ctx.ui.notify(
|
|
302
|
+
"Usage: /bar status [list|all|none|only <keys>|show <keys>|hide <keys>]",
|
|
303
|
+
"warning",
|
|
304
|
+
);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
pi.appendEntry(STATUS_FILTER_ENTRY_TYPE, serializeStatusFilter(statusFilter));
|
|
309
|
+
refresh();
|
|
310
|
+
ctx.ui.notify(`pi-bar statuses: ${describeStatusFilter(statusFilter)}`, "info");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
ctx.ui.notify(
|
|
315
|
+
"Usage: /bar status [list|all|none|only <keys>|show <keys>|hide <keys>]",
|
|
316
|
+
"warning",
|
|
317
|
+
);
|
|
318
|
+
},
|
|
319
|
+
});
|
|
189
320
|
|
|
190
321
|
pi.on("model_select", async () => refresh());
|
|
191
322
|
pi.on("thinking_level_select", async () => refresh());
|
|
192
323
|
pi.on("turn_end", async () => refresh());
|
|
193
324
|
pi.on("message_end", async () => refresh());
|
|
325
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
326
|
+
restoreStatusFilter(ctx);
|
|
327
|
+
refresh();
|
|
328
|
+
});
|
|
194
329
|
|
|
195
330
|
pi.on("session_start", async (_event, ctx) => {
|
|
331
|
+
restoreStatusFilter(ctx);
|
|
332
|
+
|
|
196
333
|
if (!ctx.hasUI) return;
|
|
197
334
|
|
|
198
335
|
const visibleSegments = parseSegments();
|
|
@@ -213,6 +350,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
213
350
|
const thinkingLevel = String(pi.getThinkingLevel());
|
|
214
351
|
const extensionStatuses = formatExtensionStatuses(
|
|
215
352
|
footerData?.getExtensionStatuses?.() ?? new Map(),
|
|
353
|
+
statusFilter,
|
|
354
|
+
seenStatusKeys,
|
|
216
355
|
);
|
|
217
356
|
const usage = ctx.getContextUsage();
|
|
218
357
|
const contextSegmentColor = contextColor(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bar",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "Never accidentally run Opus on a typo. Pi footer/statusline showing active model, thinking level, context usage, and extension statuses.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -46,6 +46,6 @@
|
|
|
46
46
|
"extensions": [
|
|
47
47
|
"./extensions/status-footer.ts"
|
|
48
48
|
],
|
|
49
|
-
"image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.
|
|
49
|
+
"image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.3/assets/screenshot.png"
|
|
50
50
|
}
|
|
51
51
|
}
|