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 CHANGED
@@ -9,7 +9,7 @@
9
9
  ◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M ❯ ✧ plan:active
10
10
  ```
11
11
 
12
- ![pi-bar screenshot, three rows showing green, yellow, and red context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.2/assets/screenshot.png)
12
+ ![pi-bar screenshot, three rows showing green, yellow, and red context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.4/assets/screenshot.png)
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 type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
- import { truncateToWidth } from "@earendil-works/pi-tui";
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 formatExtensionStatuses(statuses: ReadonlyMap<string, string>): string | null {
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.2",
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.2/assets/screenshot.png"
49
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.4/assets/screenshot.png"
50
50
  }
51
51
  }