pi-bar 0.3.5 → 0.3.6

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
@@ -3,22 +3,23 @@
3
3
  [![npm version](https://img.shields.io/npm/v/pi-bar.svg)](https://www.npmjs.com/package/pi-bar)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/pi-bar.svg)](https://www.npmjs.com/package/pi-bar)
5
5
 
6
- **Never accidentally run Opus on a typo again.** pi-bar puts the active model, thinking level, context usage, and extension statuses in pi's footer so you catch wrong-model / wrong-reasoning / context-pressure problems before they cost you.
6
+ **Never accidentally run Opus on a typo again.** pi-bar puts the active model, thinking level, context usage, live TLDR, and extension statuses in pi's footer so you catch wrong-model / wrong-reasoning / context-pressure problems before they cost you.
7
7
 
8
8
  ```text
9
- ◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M ❯ ✧ plan:active
9
+ ◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M ❯ ⓘ Inspecting package structure ❯ ✧ 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.5/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.6/assets/screenshot.png)
13
13
 
14
14
  ## Why use it?
15
15
 
16
16
  - **See the active model at a glance** — catch accidental model switches before an expensive or sensitive task starts.
17
17
  - **Track thinking level in place** — immediately notice when you are using the wrong reasoning setting.
18
18
  - **Watch context pressure early** — context usage turns green, yellow, then red as you approach the limit.
19
+ - **Show pi-tldr inline** — when `pi-tldr` is installed, its current TLDR appears as a compact footer segment.
19
20
  - **Keep extension statuses visible** — statuses set with `ctx.ui.setStatus()` still appear in the custom footer.
20
21
 
21
- pi-bar is intentionally tiny: one small extension, no broad behavior changes, and no commands to learn. It replaces pi's built-in footer with a compact model / thinking / context / extension-status line that refreshes when model, thinking level, context usage, or extension statuses change.
22
+ pi-bar is intentionally tiny: one small extension, no broad behavior changes, and no commands to learn. It replaces pi's built-in footer with a compact model / thinking / context / tldr / extension-status line that refreshes when model, thinking level, context usage, pi-tldr, or extension statuses change.
22
23
 
23
24
  ## Install
24
25
 
@@ -39,11 +40,27 @@ pi-bar works out of the box, but you can tune it with environment variables befo
39
40
  ### Show or hide segments
40
41
 
41
42
  ```bash
42
- PI_BAR_SHOW=model,thinking,context,extensions pi
43
+ PI_BAR_SHOW=model,thinking,context,tldr,extensions pi
43
44
  PI_BAR_SHOW=model,context pi
44
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
+ Allowed segments are `model`, `thinking`, `context`, `tldr`, and `extensions`. The `tldr` segment is hidden unless `pi-tldr` has a current TLDR. The `extensions` segment is hidden when no extension has set a status.
48
+
49
+ ### Show pi-tldr in the footer
50
+
51
+ Install [pi-tldr](https://www.npmjs.com/package/pi-tldr) alongside pi-bar:
52
+
53
+ ```bash
54
+ pi install npm:pi-tldr
55
+ ```
56
+
57
+ pi-bar captures pi-tldr's `pi-tldr` widget and renders its current text as the `tldr` segment. Limit segment width with:
58
+
59
+ ```bash
60
+ PI_BAR_TLDR_MAX_WIDTH=48 pi
61
+ ```
62
+
63
+ Default is `64`. Set `PI_BAR_SHOW` without `tldr` to hide it.
47
64
 
48
65
  ### Configure extension statuses
49
66
 
@@ -76,10 +93,10 @@ PI_BAR_ICONS=nerdfont pi
76
93
 
77
94
  Supported presets:
78
95
 
79
- - `unicode` — default, e.g. `◈ gpt-5.5 ❯ ✦ think:medium ❯ ◷ 42.7% / 272k ❯ ✧ plan:active`
80
- - `plain` — maximum compatibility, e.g. `model:gpt-5.5 | think:medium | ctx:42.7% / 272k | ext:plan:active`
81
- - `emoji` — colorful emoji markers, e.g. `🤖 gpt-5.5 › ✨ think:medium › 🧭 42.7% / 272k › 🧩 plan:active`
82
- - `nerdfont` — icon-font glyphs for terminals configured with a Nerd Font, e.g. `󰚩 gpt-5.5  󰧑 think:medium  󰍛 42.7% / 272k  󰐱 plan:active`
96
+ - `unicode` — default, e.g. `◈ gpt-5.5 ❯ ✦ think:medium ❯ ◷ 42.7% / 272k ❯ ⓘ Reading files ❯ ✧ plan:active`
97
+ - `plain` — maximum compatibility, e.g. `model:gpt-5.5 | think:medium | ctx:42.7% / 272k | tldr:Reading files | ext:plan:active`
98
+ - `emoji` — colorful emoji markers, e.g. `🤖 gpt-5.5 › ✨ think:medium › 🧭 42.7% / 272k › 📝 Reading files › 🧩 plan:active`
99
+ - `nerdfont` — icon-font glyphs for terminals configured with a Nerd Font, e.g. `󰚩 gpt-5.5  󰧑 think:medium  󰍛 42.7% / 272k  󰦨 Reading files  󰐱 plan:active`
83
100
 
84
101
  ## Compatibility
85
102
 
@@ -91,6 +108,7 @@ PI_BAR_ICONS=plain pi
91
108
 
92
109
  ## Pairs well with
93
110
 
111
+ - **[pi-tldr](https://www.npmjs.com/package/pi-tldr)** — show live summaries above the input bar; pi-bar mirrors them into the footer.
94
112
  - **[pi-chrome](https://www.npmjs.com/package/pi-chrome)** — give your Pi agent your real, signed-in Chrome. Use pi-bar's red-context threshold as the signal to wrap up long browser scrapes before context overflows.
95
113
  - **[pi-qq](https://www.npmjs.com/package/pi-qq)** — ask side questions about what the agent just did without polluting the transcript.
96
114
 
@@ -2,7 +2,7 @@
2
2
  * pi-bar — footer/statusline extension.
3
3
  *
4
4
  * Replaces the built-in footer with left-aligned segments:
5
- * ◈ <model name> ❯ ✦ think:<level> ❯ ◷ <context% / window> ❯ ✧ <extension statuses>
5
+ * ◈ <model name> ❯ ✦ think:<level> ❯ ◷ <context% / window> ❯ ⓘ <tldr> ❯ ✧ <extension statuses>
6
6
  *
7
7
  * Examples:
8
8
  * ◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M
@@ -11,9 +11,10 @@
11
11
  * and after each assistant turn so context usage stays current.
12
12
  *
13
13
  * Configuration env vars:
14
- * PI_BAR_SHOW=model,thinking,context,extensions
14
+ * PI_BAR_SHOW=model,thinking,context,tldr,extensions
15
15
  * PI_BAR_THRESHOLDS=70,90
16
16
  * PI_BAR_ICONS=unicode|plain|emoji|nerdfont
17
+ * PI_BAR_TLDR_MAX_WIDTH=64
17
18
  */
18
19
 
19
20
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -23,15 +24,19 @@ import {
23
24
  getSettingsListTheme,
24
25
  type ExtensionAPI,
25
26
  type ExtensionContext,
27
+ type Theme,
28
+ type ThemeColor,
26
29
  } from "@earendil-works/pi-coding-agent";
27
30
  import {
28
31
  Container,
32
+ type Component,
29
33
  type SettingItem,
30
34
  SettingsList,
35
+ type TUI,
31
36
  truncateToWidth,
32
37
  } from "@earendil-works/pi-tui";
33
38
 
34
- type SegmentName = "model" | "thinking" | "context" | "extensions";
39
+ type SegmentName = "model" | "thinking" | "context" | "tldr" | "extensions";
35
40
  type IconPreset = "unicode" | "plain" | "emoji" | "nerdfont";
36
41
  type StatusFilter =
37
42
  | { mode: "all"; hidden: Set<string> }
@@ -39,8 +44,17 @@ type StatusFilter =
39
44
  type SerializedStatusFilter =
40
45
  | { mode: "all"; hidden: string[] }
41
46
  | { mode: "only"; shown: string[] };
47
+ type TldrWidgetFactory = (tui: TUI, theme: Theme) => Component & {
48
+ dispose?(): void;
49
+ };
50
+ type CapturedTldrWidget =
51
+ | { kind: "lines"; lines: string[] }
52
+ | { kind: "factory"; factory: TldrWidgetFactory };
42
53
 
43
54
  const STATUS_FILTER_ENTRY_TYPE = "pi-bar-status-filter";
55
+ const TLDR_WIDGET_KEY = "pi-tldr";
56
+ const DEFAULT_TLDR_MAX_WIDTH = 64;
57
+ const ANSI_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g;
44
58
  const CONFIG_PATH =
45
59
  process.env.PI_BAR_CONFIG ?? join(homedir(), ".pi", "agent", "pi-bar.json");
46
60
 
@@ -48,6 +62,7 @@ const DEFAULT_SEGMENTS: SegmentName[] = [
48
62
  "model",
49
63
  "thinking",
50
64
  "context",
65
+ "tldr",
51
66
  "extensions",
52
67
  ];
53
68
  const DEFAULT_WARNING_THRESHOLD = 70;
@@ -59,6 +74,7 @@ const ICON_PRESETS: Record<
59
74
  model: string;
60
75
  thinking: string;
61
76
  context: string;
77
+ tldr: string;
62
78
  extensions: string;
63
79
  separator: string;
64
80
  }
@@ -67,6 +83,7 @@ const ICON_PRESETS: Record<
67
83
  model: "◈",
68
84
  thinking: "✦",
69
85
  context: "◷",
86
+ tldr: "ⓘ",
70
87
  extensions: "✧",
71
88
  separator: "❯",
72
89
  },
@@ -74,6 +91,7 @@ const ICON_PRESETS: Record<
74
91
  model: "model",
75
92
  thinking: "think",
76
93
  context: "ctx",
94
+ tldr: "tldr",
77
95
  extensions: "ext",
78
96
  separator: "|",
79
97
  },
@@ -81,6 +99,7 @@ const ICON_PRESETS: Record<
81
99
  model: "🤖",
82
100
  thinking: "✨",
83
101
  context: "🧭",
102
+ tldr: "📝",
84
103
  extensions: "🧩",
85
104
  separator: "›",
86
105
  },
@@ -88,6 +107,7 @@ const ICON_PRESETS: Record<
88
107
  model: "󰚩",
89
108
  thinking: "󰧑",
90
109
  context: "󰍛",
110
+ tldr: "󰦨",
91
111
  extensions: "󰐱",
92
112
  separator: "",
93
113
  },
@@ -111,7 +131,7 @@ function formatModelName(id: string | undefined): string {
111
131
  return base.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
112
132
  }
113
133
 
114
- function thinkingColor(level: string): string {
134
+ function thinkingColor(level: string): ThemeColor {
115
135
  switch (level) {
116
136
  case "off":
117
137
  return "thinkingOff";
@@ -137,7 +157,7 @@ function contextColor(
137
157
  percent: number | null | undefined,
138
158
  warningThreshold: number,
139
159
  errorThreshold: number,
140
- ): string {
160
+ ): ThemeColor {
141
161
  if (percent === null || percent === undefined) return "muted";
142
162
  if (percent >= errorThreshold) return "error";
143
163
  if (percent >= warningThreshold) return "warning";
@@ -152,7 +172,7 @@ function parseSegments(): SegmentName[] {
152
172
  .split(",")
153
173
  .map((segment) => segment.trim().toLowerCase())
154
174
  .filter((segment): segment is SegmentName =>
155
- ["model", "thinking", "context", "extensions"].includes(segment),
175
+ ["model", "thinking", "context", "tldr", "extensions"].includes(segment),
156
176
  );
157
177
 
158
178
  return requested.length > 0 ? requested : DEFAULT_SEGMENTS;
@@ -192,6 +212,63 @@ function parseIconPreset(): IconPreset {
192
212
  return "unicode";
193
213
  }
194
214
 
215
+ function parseTldrMaxWidth(): number {
216
+ const raw = process.env.PI_BAR_TLDR_MAX_WIDTH;
217
+ if (!raw) return DEFAULT_TLDR_MAX_WIDTH;
218
+
219
+ const value = Number.parseInt(raw, 10);
220
+ if (Number.isFinite(value) && value >= 10 && value <= 500) return value;
221
+ return DEFAULT_TLDR_MAX_WIDTH;
222
+ }
223
+
224
+ function stripAnsi(value: string): string {
225
+ return value.replace(ANSI_PATTERN, "");
226
+ }
227
+
228
+ function extractTldrText(lines: readonly string[]): string | null {
229
+ const parts = lines
230
+ .map(stripAnsi)
231
+ .map((line) => line.trimEnd())
232
+ .filter((line) => line.trim().length > 0)
233
+ .filter((line) => !/^[╭╰].*[╮╯]$/.test(line.trim()))
234
+ .map((line) => {
235
+ const trimmed = line.trim();
236
+ if (trimmed.startsWith("│") && trimmed.endsWith("│")) {
237
+ return trimmed.slice(1, -1).trim();
238
+ }
239
+ return trimmed.replace(/^tldr:\s*/i, "").trim();
240
+ })
241
+ .filter(Boolean);
242
+
243
+ const text = parts.join(" ").replace(/\s+/g, " ").trim();
244
+ return text.length > 0 ? text : null;
245
+ }
246
+
247
+ function renderCapturedTldr(
248
+ widget: CapturedTldrWidget | undefined,
249
+ tui: TUI,
250
+ theme: Theme,
251
+ width: number,
252
+ maxWidth: number,
253
+ ): string | null {
254
+ if (!widget) return null;
255
+
256
+ let lines: string[];
257
+ if (widget.kind === "lines") {
258
+ lines = widget.lines;
259
+ } else {
260
+ const component = widget.factory(tui, theme);
261
+ try {
262
+ lines = component.render(Math.max(12, width));
263
+ } finally {
264
+ component.dispose?.();
265
+ }
266
+ }
267
+
268
+ const text = extractTldrText(lines);
269
+ return text ? truncateToWidth(text, maxWidth) : null;
270
+ }
271
+
195
272
  function prefixed(icon: string, text: string, preset: IconPreset): string {
196
273
  if (preset === "plain") return `${icon}:${text}`;
197
274
  return `${icon} ${text}`;
@@ -291,8 +368,39 @@ function getKnownStatusKeys(filter: StatusFilter, seenStatusKeys: Set<string>):
291
368
  export default function (pi: ExtensionAPI) {
292
369
  let requestRender: (() => void) | undefined;
293
370
  let statusFilter: StatusFilter = { mode: "all", hidden: new Set() };
371
+ let capturedTldrWidget: CapturedTldrWidget | undefined;
294
372
  const seenStatusKeys = new Set<string>();
373
+ const patchedUis = new WeakSet<object>();
295
374
  const refresh = () => requestRender?.();
375
+ const installTldrWidgetCapture = (ctx: ExtensionContext) => {
376
+ if (!ctx.hasUI) return;
377
+ const ui = ctx.ui as ExtensionContext["ui"];
378
+ if (patchedUis.has(ui)) return;
379
+ patchedUis.add(ui);
380
+
381
+ const originalSetWidget = ui.setWidget.bind(ui);
382
+ ui.setWidget = ((key: string, content: unknown, options?: unknown) => {
383
+ if (key === TLDR_WIDGET_KEY) {
384
+ if (Array.isArray(content)) {
385
+ capturedTldrWidget = { kind: "lines", lines: content };
386
+ } else if (typeof content === "function") {
387
+ capturedTldrWidget = {
388
+ kind: "factory",
389
+ factory: content as TldrWidgetFactory,
390
+ };
391
+ } else {
392
+ capturedTldrWidget = undefined;
393
+ }
394
+ refresh();
395
+ }
396
+
397
+ return originalSetWidget(
398
+ key,
399
+ content as Parameters<ExtensionContext["ui"]["setWidget"]>[1],
400
+ options as Parameters<ExtensionContext["ui"]["setWidget"]>[2],
401
+ );
402
+ }) as ExtensionContext["ui"]["setWidget"];
403
+ };
296
404
  const restoreStatusFilter = (ctx: ExtensionContext) => {
297
405
  let restoredFilter = readGlobalStatusFilter();
298
406
  if (!restoredFilter) {
@@ -481,11 +589,14 @@ export default function (pi: ExtensionAPI) {
481
589
  pi.on("turn_end", async () => refresh());
482
590
  pi.on("message_end", async () => refresh());
483
591
  pi.on("session_tree", async (_event, ctx) => {
592
+ installTldrWidgetCapture(ctx);
484
593
  restoreStatusFilter(ctx);
485
594
  refresh();
486
595
  });
487
596
 
488
597
  pi.on("session_start", async (_event, ctx) => {
598
+ capturedTldrWidget = undefined;
599
+ installTldrWidgetCapture(ctx);
489
600
  restoreStatusFilter(ctx);
490
601
 
491
602
  if (!ctx.hasUI) return;
@@ -494,6 +605,7 @@ export default function (pi: ExtensionAPI) {
494
605
  const iconPreset = parseIconPreset();
495
606
  const icons = ICON_PRESETS[iconPreset];
496
607
  const { warningThreshold, errorThreshold } = parseThresholds();
608
+ const tldrMaxWidth = parseTldrMaxWidth();
497
609
 
498
610
  ctx.ui.setFooter((tui, theme, footerData) => {
499
611
  requestRender = () => tui.requestRender();
@@ -521,6 +633,13 @@ export default function (pi: ExtensionAPI) {
521
633
  const contextText = usage
522
634
  ? `${usage.percent !== null ? `${usage.percent.toFixed(1)}%` : "—%"} / ${formatTokens(usage.contextWindow)}`
523
635
  : "—";
636
+ const tldrText = renderCapturedTldr(
637
+ capturedTldrWidget,
638
+ tui,
639
+ theme,
640
+ width,
641
+ tldrMaxWidth,
642
+ );
524
643
 
525
644
  const segmentRenderers: Record<SegmentName, string | null> = {
526
645
  model: theme.fg(
@@ -535,6 +654,9 @@ export default function (pi: ExtensionAPI) {
535
654
  contextSegmentColor,
536
655
  prefixed(icons.context, contextText, iconPreset),
537
656
  ),
657
+ tldr: tldrText
658
+ ? theme.fg("muted", prefixed(icons.tldr, tldrText, iconPreset))
659
+ : null,
538
660
  extensions: extensionStatuses
539
661
  ? theme.fg(
540
662
  "muted",
@@ -556,6 +678,7 @@ export default function (pi: ExtensionAPI) {
556
678
  });
557
679
 
558
680
  pi.on("session_shutdown", async (_event, ctx) => {
681
+ capturedTldrWidget = undefined;
559
682
  if (ctx.hasUI) ctx.ui.setFooter(undefined);
560
683
  });
561
684
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-bar",
3
- "version": "0.3.5",
4
- "description": "Never accidentally run Opus on a typo. Pi footer/statusline showing active model, thinking level, context usage, and extension statuses.",
3
+ "version": "0.3.6",
4
+ "description": "Never accidentally run Opus on a typo. Pi footer/statusline showing active model, thinking level, context usage, pi-tldr, and extension statuses.",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "pi-package",
@@ -13,7 +13,9 @@
13
13
  "model",
14
14
  "thinking",
15
15
  "context",
16
- "context-window"
16
+ "context-window",
17
+ "tldr",
18
+ "pi-tldr"
17
19
  ],
18
20
  "license": "MIT",
19
21
  "repository": {
@@ -46,6 +48,6 @@
46
48
  "extensions": [
47
49
  "./extensions/status-footer.ts"
48
50
  ],
49
- "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.5/assets/screenshot.png"
51
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.6/assets/screenshot.png"
50
52
  }
51
53
  }