pi-bar 0.3.2 → 0.3.4
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 +14 -1
- package/extensions/status-footer.ts +270 -3
- 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,19 @@ 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` inside pi to open an interactive status-visibility picker.
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
/bar
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Toggle each status key between `shown` and `hidden`. The `New statuses` row controls whether status keys discovered later are shown or hidden by default.
|
|
58
|
+
|
|
59
|
+
Status filter choices persist in the current session. Advanced command forms like `/bar status hide <key>` still work for scripting.
|
|
60
|
+
|
|
48
61
|
### Change context thresholds
|
|
49
62
|
|
|
50
63
|
```bash
|
|
@@ -16,11 +16,28 @@
|
|
|
16
16
|
* PI_BAR_ICONS=unicode|plain|emoji|nerdfont
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import
|
|
20
|
-
|
|
19
|
+
import {
|
|
20
|
+
getSettingsListTheme,
|
|
21
|
+
type ExtensionAPI,
|
|
22
|
+
type ExtensionContext,
|
|
23
|
+
} from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import {
|
|
25
|
+
Container,
|
|
26
|
+
type SettingItem,
|
|
27
|
+
SettingsList,
|
|
28
|
+
truncateToWidth,
|
|
29
|
+
} from "@earendil-works/pi-tui";
|
|
21
30
|
|
|
22
31
|
type SegmentName = "model" | "thinking" | "context" | "extensions";
|
|
23
32
|
type IconPreset = "unicode" | "plain" | "emoji" | "nerdfont";
|
|
33
|
+
type StatusFilter =
|
|
34
|
+
| { mode: "all"; hidden: Set<string> }
|
|
35
|
+
| { mode: "only"; shown: Set<string> };
|
|
36
|
+
type SerializedStatusFilter =
|
|
37
|
+
| { mode: "all"; hidden: string[] }
|
|
38
|
+
| { mode: "only"; shown: string[] };
|
|
39
|
+
|
|
40
|
+
const STATUS_FILTER_ENTRY_TYPE = "pi-bar-status-filter";
|
|
24
41
|
|
|
25
42
|
const DEFAULT_SEGMENTS: SegmentName[] = [
|
|
26
43
|
"model",
|
|
@@ -175,24 +192,272 @@ function prefixed(icon: string, text: string, preset: IconPreset): string {
|
|
|
175
192
|
return `${icon} ${text}`;
|
|
176
193
|
}
|
|
177
194
|
|
|
178
|
-
function
|
|
195
|
+
function shouldShowStatus(key: string, filter: StatusFilter): boolean {
|
|
196
|
+
if (filter.mode === "only") return filter.shown.has(key);
|
|
197
|
+
return !filter.hidden.has(key);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function formatExtensionStatuses(
|
|
201
|
+
statuses: ReadonlyMap<string, string>,
|
|
202
|
+
filter: StatusFilter,
|
|
203
|
+
seenStatusKeys: Set<string>,
|
|
204
|
+
): string | null {
|
|
179
205
|
const parts = Array.from(statuses.entries())
|
|
180
206
|
.filter(([, text]) => text.trim().length > 0)
|
|
207
|
+
.filter(([key]) => {
|
|
208
|
+
seenStatusKeys.add(key);
|
|
209
|
+
return shouldShowStatus(key, filter);
|
|
210
|
+
})
|
|
181
211
|
.map(([key, text]) => `${key}:${text}`);
|
|
182
212
|
|
|
183
213
|
return parts.length > 0 ? parts.join(" ") : null;
|
|
184
214
|
}
|
|
185
215
|
|
|
216
|
+
function serializeStatusFilter(filter: StatusFilter): SerializedStatusFilter {
|
|
217
|
+
if (filter.mode === "only") {
|
|
218
|
+
return { mode: "only", shown: Array.from(filter.shown).sort() };
|
|
219
|
+
}
|
|
220
|
+
return { mode: "all", hidden: Array.from(filter.hidden).sort() };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function parseSerializedStatusFilter(value: unknown): StatusFilter | null {
|
|
224
|
+
if (!value || typeof value !== "object") return null;
|
|
225
|
+
const data = value as Partial<SerializedStatusFilter>;
|
|
226
|
+
|
|
227
|
+
if (data.mode === "only" && Array.isArray(data.shown)) {
|
|
228
|
+
return { mode: "only", shown: new Set(data.shown) };
|
|
229
|
+
}
|
|
230
|
+
if (data.mode === "all" && Array.isArray(data.hidden)) {
|
|
231
|
+
return { mode: "all", hidden: new Set(data.hidden) };
|
|
232
|
+
}
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function splitStatusKeys(raw: string): string[] {
|
|
237
|
+
return raw
|
|
238
|
+
.split(/[\s,]+/)
|
|
239
|
+
.map((key) => key.trim())
|
|
240
|
+
.filter(Boolean);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function describeStatusFilter(filter: StatusFilter): string {
|
|
244
|
+
if (filter.mode === "only") {
|
|
245
|
+
const shown = Array.from(filter.shown).sort();
|
|
246
|
+
return shown.length > 0 ? `showing only: ${shown.join(", ")}` : "showing none";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const hidden = Array.from(filter.hidden).sort();
|
|
250
|
+
return hidden.length > 0 ? `showing all except: ${hidden.join(", ")}` : "showing all";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function getKnownStatusKeys(filter: StatusFilter, seenStatusKeys: Set<string>): string[] {
|
|
254
|
+
const keys = new Set(seenStatusKeys);
|
|
255
|
+
if (filter.mode === "only") {
|
|
256
|
+
for (const key of filter.shown) keys.add(key);
|
|
257
|
+
} else {
|
|
258
|
+
for (const key of filter.hidden) keys.add(key);
|
|
259
|
+
}
|
|
260
|
+
return Array.from(keys).sort();
|
|
261
|
+
}
|
|
262
|
+
|
|
186
263
|
export default function (pi: ExtensionAPI) {
|
|
187
264
|
let requestRender: (() => void) | undefined;
|
|
265
|
+
let statusFilter: StatusFilter = { mode: "all", hidden: new Set() };
|
|
266
|
+
const seenStatusKeys = new Set<string>();
|
|
188
267
|
const refresh = () => requestRender?.();
|
|
268
|
+
const restoreStatusFilter = (ctx: ExtensionContext) => {
|
|
269
|
+
let restoredFilter: StatusFilter | null = null;
|
|
270
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
271
|
+
if (entry.type === "custom" && entry.customType === STATUS_FILTER_ENTRY_TYPE) {
|
|
272
|
+
restoredFilter = parseSerializedStatusFilter(entry.data);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
statusFilter = restoredFilter ?? { mode: "all", hidden: new Set() };
|
|
276
|
+
};
|
|
277
|
+
const persistStatusFilter = () => {
|
|
278
|
+
pi.appendEntry(STATUS_FILTER_ENTRY_TYPE, serializeStatusFilter(statusFilter));
|
|
279
|
+
refresh();
|
|
280
|
+
};
|
|
281
|
+
const openStatusConfigurator = async (ctx: ExtensionContext) => {
|
|
282
|
+
const knownStatusKeys = getKnownStatusKeys(statusFilter, seenStatusKeys);
|
|
283
|
+
if (knownStatusKeys.length === 0) {
|
|
284
|
+
ctx.ui.notify(
|
|
285
|
+
"No extension statuses seen yet. Open /bar after another extension calls ctx.ui.setStatus().",
|
|
286
|
+
"info",
|
|
287
|
+
);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
await ctx.ui.custom((tui, theme, _kb, done) => {
|
|
292
|
+
let futureShown = statusFilter.mode === "all";
|
|
293
|
+
const statusVisibility = new Map(
|
|
294
|
+
knownStatusKeys.map((key) => [key, shouldShowStatus(key, statusFilter)]),
|
|
295
|
+
);
|
|
296
|
+
const persistFromVisibility = () => {
|
|
297
|
+
if (futureShown) {
|
|
298
|
+
statusFilter = {
|
|
299
|
+
mode: "all",
|
|
300
|
+
hidden: new Set(
|
|
301
|
+
knownStatusKeys.filter((key) => !statusVisibility.get(key)),
|
|
302
|
+
),
|
|
303
|
+
};
|
|
304
|
+
} else {
|
|
305
|
+
statusFilter = {
|
|
306
|
+
mode: "only",
|
|
307
|
+
shown: new Set(
|
|
308
|
+
knownStatusKeys.filter((key) => statusVisibility.get(key)),
|
|
309
|
+
),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
persistStatusFilter();
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const items: SettingItem[] = [
|
|
316
|
+
{
|
|
317
|
+
id: "__future",
|
|
318
|
+
label: "New statuses",
|
|
319
|
+
description: "Default visibility for status keys discovered later",
|
|
320
|
+
currentValue: futureShown ? "shown" : "hidden",
|
|
321
|
+
values: ["shown", "hidden"],
|
|
322
|
+
},
|
|
323
|
+
...knownStatusKeys.map((key): SettingItem => ({
|
|
324
|
+
id: key,
|
|
325
|
+
label: key,
|
|
326
|
+
description: "Extension status visibility",
|
|
327
|
+
currentValue: statusVisibility.get(key) ? "shown" : "hidden",
|
|
328
|
+
values: ["shown", "hidden"],
|
|
329
|
+
})),
|
|
330
|
+
];
|
|
331
|
+
|
|
332
|
+
const container = new Container();
|
|
333
|
+
container.addChild(
|
|
334
|
+
new (class {
|
|
335
|
+
render(_width: number) {
|
|
336
|
+
return [
|
|
337
|
+
theme.fg("accent", theme.bold("pi-bar status visibility")),
|
|
338
|
+
theme.fg("dim", "Enter/Space toggles · Esc closes"),
|
|
339
|
+
"",
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
invalidate() {}
|
|
343
|
+
})(),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
const settingsList = new SettingsList(
|
|
347
|
+
items,
|
|
348
|
+
Math.min(items.length + 2, 15),
|
|
349
|
+
getSettingsListTheme(),
|
|
350
|
+
(id, newValue) => {
|
|
351
|
+
if (id === "__future") {
|
|
352
|
+
futureShown = newValue === "shown";
|
|
353
|
+
} else {
|
|
354
|
+
statusVisibility.set(id, newValue === "shown");
|
|
355
|
+
}
|
|
356
|
+
persistFromVisibility();
|
|
357
|
+
},
|
|
358
|
+
() => done(undefined),
|
|
359
|
+
{ enableSearch: true },
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
container.addChild(settingsList);
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
render(width: number) {
|
|
366
|
+
return container.render(width);
|
|
367
|
+
},
|
|
368
|
+
invalidate() {
|
|
369
|
+
container.invalidate();
|
|
370
|
+
},
|
|
371
|
+
handleInput(data: string) {
|
|
372
|
+
settingsList.handleInput?.(data);
|
|
373
|
+
tui.requestRender();
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
pi.registerCommand("bar", {
|
|
380
|
+
description: "Configure pi-bar footer status segments",
|
|
381
|
+
handler: async (args, ctx) => {
|
|
382
|
+
const [section, action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
383
|
+
if (!section || ((section === "status" || section === "statuses") && !action)) {
|
|
384
|
+
await openStatusConfigurator(ctx);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (section === "status" || section === "statuses") {
|
|
389
|
+
const keys = splitStatusKeys(rest.join(" "));
|
|
390
|
+
|
|
391
|
+
switch (action) {
|
|
392
|
+
case "config":
|
|
393
|
+
case "configure":
|
|
394
|
+
case "edit":
|
|
395
|
+
await openStatusConfigurator(ctx);
|
|
396
|
+
return;
|
|
397
|
+
case "list":
|
|
398
|
+
case "ls": {
|
|
399
|
+
const known = getKnownStatusKeys(statusFilter, seenStatusKeys);
|
|
400
|
+
ctx.ui.notify(
|
|
401
|
+
`pi-bar statuses: ${describeStatusFilter(statusFilter)}${known.length > 0 ? `; known: ${known.join(", ")}` : "; known: none yet"}`,
|
|
402
|
+
"info",
|
|
403
|
+
);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
case "all":
|
|
407
|
+
statusFilter = { mode: "all", hidden: new Set() };
|
|
408
|
+
break;
|
|
409
|
+
case "none":
|
|
410
|
+
statusFilter = { mode: "only", shown: new Set() };
|
|
411
|
+
break;
|
|
412
|
+
case "only":
|
|
413
|
+
statusFilter = { mode: "only", shown: new Set(keys) };
|
|
414
|
+
break;
|
|
415
|
+
case "hide":
|
|
416
|
+
if (statusFilter.mode === "only") {
|
|
417
|
+
for (const key of keys) statusFilter.shown.delete(key);
|
|
418
|
+
} else {
|
|
419
|
+
for (const key of keys) statusFilter.hidden.add(key);
|
|
420
|
+
}
|
|
421
|
+
break;
|
|
422
|
+
case "show":
|
|
423
|
+
if (statusFilter.mode === "only") {
|
|
424
|
+
for (const key of keys) statusFilter.shown.add(key);
|
|
425
|
+
} else {
|
|
426
|
+
for (const key of keys) statusFilter.hidden.delete(key);
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
default:
|
|
430
|
+
ctx.ui.notify(
|
|
431
|
+
"Usage: /bar status [list|all|none|only <keys>|show <keys>|hide <keys>]",
|
|
432
|
+
"warning",
|
|
433
|
+
);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
persistStatusFilter();
|
|
438
|
+
ctx.ui.notify(`pi-bar statuses: ${describeStatusFilter(statusFilter)}`, "info");
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
ctx.ui.notify(
|
|
443
|
+
"Usage: /bar status [list|all|none|only <keys>|show <keys>|hide <keys>]",
|
|
444
|
+
"warning",
|
|
445
|
+
);
|
|
446
|
+
},
|
|
447
|
+
});
|
|
189
448
|
|
|
190
449
|
pi.on("model_select", async () => refresh());
|
|
191
450
|
pi.on("thinking_level_select", async () => refresh());
|
|
192
451
|
pi.on("turn_end", async () => refresh());
|
|
193
452
|
pi.on("message_end", async () => refresh());
|
|
453
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
454
|
+
restoreStatusFilter(ctx);
|
|
455
|
+
refresh();
|
|
456
|
+
});
|
|
194
457
|
|
|
195
458
|
pi.on("session_start", async (_event, ctx) => {
|
|
459
|
+
restoreStatusFilter(ctx);
|
|
460
|
+
|
|
196
461
|
if (!ctx.hasUI) return;
|
|
197
462
|
|
|
198
463
|
const visibleSegments = parseSegments();
|
|
@@ -213,6 +478,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
213
478
|
const thinkingLevel = String(pi.getThinkingLevel());
|
|
214
479
|
const extensionStatuses = formatExtensionStatuses(
|
|
215
480
|
footerData?.getExtensionStatuses?.() ?? new Map(),
|
|
481
|
+
statusFilter,
|
|
482
|
+
seenStatusKeys,
|
|
216
483
|
);
|
|
217
484
|
const usage = ctx.getContextUsage();
|
|
218
485
|
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.4",
|
|
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.4/assets/screenshot.png"
|
|
50
50
|
}
|
|
51
51
|
}
|