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 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.3/assets/screenshot.png)
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 formatExtensionStatuses(statuses: ReadonlyMap<string, string>): string | null {
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.2",
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.2/assets/screenshot.png"
49
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.3/assets/screenshot.png"
50
50
  }
51
51
  }