pi-bar 0.3.13 → 0.3.15

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,23 +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, live TLDR, 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 keeps your model, thinking level, context pressure, extension status, and a live one-line TLDR visible in pi's footer.
7
7
 
8
8
  ```text
9
- ◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M ❯ Inspecting package structure ❯ ✧ 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.13/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.15/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 live TLDR inline** — pi-bar can generate a compact live summary directly in the footer.
19
+ - **Follow what pi is doing** — a live TLDR keeps the current task visible without scrolling.
20
20
  - **Keep extension statuses visible** — statuses set with `ctx.ui.setStatus()` still appear in the custom footer.
21
21
 
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, generated TLDR, 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.
23
23
 
24
24
  ## Install
25
25
 
@@ -48,15 +48,15 @@ Allowed segments are `model`, `thinking`, `context`, `tldr`, and `extensions`. T
48
48
 
49
49
  ### Configure live TLDR
50
50
 
51
- pi-bar generates concise live footer TLDR fragments from recent prompt, assistant, tool, and result activity. TLDR state resets when you navigate the session tree so stale summaries do not follow you across branches. It uses a fast authenticated model and stays quiet when none is available. Set `PI_BAR_SHOW` without `tldr` to hide it.
51
+ pi-bar shows a concise live TLDR of the current task in the footer. It generates TLDRs from compacted recent prompt, assistant, tool, and result activity. It resets when you navigate the session tree so stale summaries do not follow you across branches. Set `PI_BAR_SHOW` without `tldr` to hide it.
52
52
 
53
- By default, pi-bar uses the first authenticated fast model from its fallback list. Override with:
53
+ Override the TLDR model with:
54
54
 
55
55
  ```bash
56
56
  PI_BAR_TLDR_MODEL=anthropic/claude-haiku-4-5 pi
57
57
  ```
58
58
 
59
- You can also set `bar.tldrModel` in pi settings; `tldr.model` is accepted for compatibility with existing settings.
59
+ You can also set `bar.tldrModel` in pi settings.
60
60
 
61
61
  ### Configure extension statuses
62
62
 
@@ -79,29 +79,6 @@ PI_BAR_THRESHOLDS=60,85 pi
79
79
 
80
80
  The first number is the warning/yellow threshold. The second number is the danger/red threshold. Defaults are `70,90`.
81
81
 
82
- ### Change icon style
83
-
84
- ```bash
85
- PI_BAR_ICONS=plain pi
86
- PI_BAR_ICONS=emoji pi
87
- PI_BAR_ICONS=nerdfont pi
88
- ```
89
-
90
- Supported presets:
91
-
92
- - `unicode` — default, e.g. `◈ gpt-5.5 ❯ ✦ think:medium ❯ ◷ 42.7% / 272k ❯ Reading files ❯ ✧ plan:active`
93
- - `plain` — maximum compatibility, e.g. `model:gpt-5.5 | think:medium | ctx:42.7% / 272k | Reading files | ext:plan:active`
94
- - `emoji` — colorful emoji markers, e.g. `🤖 gpt-5.5 › ✨ think:medium › 🧭 42.7% / 272k › Reading files › 🧩 plan:active`
95
- - `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`
96
-
97
- ## Compatibility
98
-
99
- The default `unicode` preset uses common Unicode symbols that render well in most modern terminals. If your terminal shows boxes or misaligned glyphs, use:
100
-
101
- ```bash
102
- PI_BAR_ICONS=plain pi
103
- ```
104
-
105
82
  ## Pairs well with
106
83
 
107
84
  - **[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.
@@ -111,4 +88,4 @@ PI_BAR_ICONS=plain pi
111
88
 
112
89
  Pi extensions run with your local user permissions. Review any pi package source before installing it.
113
90
 
114
- Live TLDR generation sends short snippets from recent prompt, assistant, tool, and result activity to the selected TLDR model provider. Disable the `tldr` segment with `PI_BAR_SHOW` if that is unacceptable.
91
+ Live TLDR generation sends short activity snippets to the selected TLDR model provider. Disable the `tldr` segment with `PI_BAR_SHOW` if that is unacceptable.
@@ -2,10 +2,10 @@
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> ❯ <tldr> ❯ ✧ <extension statuses>
5
+ * <model name> ❯ think:<level> ❯ <context% / window> ❯ <tldr> ❯ <extension statuses>
6
6
  *
7
7
  * Examples:
8
- * ◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M
8
+ * claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M
9
9
  *
10
10
  * Re-renders on model change, thinking-level change, status updates,
11
11
  * and after each assistant turn so context usage stays current.
@@ -13,7 +13,6 @@
13
13
  * Configuration env vars:
14
14
  * PI_BAR_SHOW=model,thinking,context,tldr,extensions
15
15
  * PI_BAR_THRESHOLDS=70,90
16
- * PI_BAR_ICONS=unicode|plain|emoji|nerdfont
17
16
  */
18
17
 
19
18
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
@@ -35,7 +34,6 @@ import {
35
34
  } from "@earendil-works/pi-tui";
36
35
 
37
36
  type SegmentName = "model" | "thinking" | "context" | "tldr" | "extensions";
38
- type IconPreset = "unicode" | "plain" | "emoji" | "nerdfont";
39
37
  type StatusFilter =
40
38
  | { mode: "all"; hidden: Set<string> }
41
39
  | { mode: "only"; shown: Set<string> };
@@ -76,8 +74,14 @@ type FastModelAuth = {
76
74
  const STATUS_FILTER_ENTRY_TYPE = "pi-bar-status-filter";
77
75
  const SETTINGS_TLDR_KEY = "tldr";
78
76
  const SETTINGS_BAR_KEY = "bar";
79
- const MAX_ACTIVITY_TEXT_CHARS = 1_500;
77
+ const MAX_ACTIVITY_TEXT_CHARS = 800;
78
+ const MAX_USER_TEXT_CHARS = 700;
79
+ const MAX_ASSISTANT_UPDATE_CHARS = 500;
80
+ const MAX_TOOL_RESULT_OK_CHARS = 500;
81
+ const MAX_TOOL_RESULT_ERROR_CHARS = 900;
82
+ const MAX_FINAL_TEXT_CHARS = 700;
80
83
  const MAX_RETAINED_RAW_ACTIVITIES = 128;
84
+ const ANSI_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g;
81
85
  const MAX_CONTEXT_CHECKPOINTS = 8;
82
86
  const TLDR_MAX_TOKENS = 120;
83
87
  const TLDR_REQUEST_TIMEOUT_MS = 2_000;
@@ -96,45 +100,7 @@ const DEFAULT_SEGMENTS: SegmentName[] = [
96
100
  const DEFAULT_WARNING_THRESHOLD = 70;
97
101
  const DEFAULT_ERROR_THRESHOLD = 90;
98
102
 
99
- const ICON_PRESETS: Record<
100
- IconPreset,
101
- {
102
- model: string;
103
- thinking: string;
104
- context: string;
105
- extensions: string;
106
- separator: string;
107
- }
108
- > = {
109
- unicode: {
110
- model: "◈",
111
- thinking: "✦",
112
- context: "◷",
113
- extensions: "✧",
114
- separator: "❯",
115
- },
116
- plain: {
117
- model: "model",
118
- thinking: "think",
119
- context: "ctx",
120
- extensions: "ext",
121
- separator: "|",
122
- },
123
- emoji: {
124
- model: "🤖",
125
- thinking: "✨",
126
- context: "🧭",
127
- extensions: "🧩",
128
- separator: "›",
129
- },
130
- nerdfont: {
131
- model: "󰚩",
132
- thinking: "󰧑",
133
- context: "󰍛",
134
- extensions: "󰐱",
135
- separator: "",
136
- },
137
- };
103
+ const SEGMENT_SEPARATOR = "❯";
138
104
 
139
105
  function formatTokens(n: number): string {
140
106
  if (n >= 1_000_000) {
@@ -229,12 +195,6 @@ function parseThresholds(): { warningThreshold: number; errorThreshold: number }
229
195
  };
230
196
  }
231
197
 
232
- function parseIconPreset(): IconPreset {
233
- const raw = process.env.PI_BAR_ICONS?.trim().toLowerCase();
234
- if (raw === "plain" || raw === "emoji" || raw === "nerdfont") return raw;
235
- return "unicode";
236
- }
237
-
238
198
  function parseTldrModelSpec(value: string): TldrModelPreference | undefined {
239
199
  const trimmed = value.trim();
240
200
  if (!trimmed || trimmed === "auto") return undefined;
@@ -324,6 +284,76 @@ function truncateText(text: string, maxChars: number): string {
324
284
  return `${text.slice(0, maxChars - 1)}…`;
325
285
  }
326
286
 
287
+ function tailText(text: string, maxChars: number): string {
288
+ if (text.length <= maxChars) return text;
289
+ return `…${text.slice(-(maxChars - 1))}`;
290
+ }
291
+
292
+ function compactText(text: string): string {
293
+ return text.replace(ANSI_PATTERN, "").replace(/\s+/g, " ").trim();
294
+ }
295
+
296
+ function compactValue(value: unknown, maxChars: number): string | undefined {
297
+ if (value === undefined || value === null) return undefined;
298
+ let text: string;
299
+ if (typeof value === "string") {
300
+ text = value;
301
+ } else {
302
+ try {
303
+ text = JSON.stringify(value);
304
+ } catch {
305
+ text = String(value);
306
+ }
307
+ }
308
+ return text ? truncateText(compactText(text), maxChars) : undefined;
309
+ }
310
+
311
+ function compactToolInput(toolName: string, input: Record<string, unknown> | undefined): string {
312
+ if (!input) return toolName;
313
+
314
+ const path = compactValue(input.path ?? input.filePath ?? input.cwd, 120);
315
+ const command = compactValue(input.command, 180);
316
+ const pattern = compactValue(input.pattern ?? input.query, 120);
317
+ const include = compactValue(input.include ?? input.glob, 80);
318
+
319
+ switch (toolName) {
320
+ case "bash":
321
+ return command ? `bash ${command}` : "bash";
322
+ case "read":
323
+ return path ? `read ${path}` : "read";
324
+ case "edit":
325
+ return path ? `edit ${path}` : "edit";
326
+ case "write":
327
+ return path ? `write ${path}` : "write";
328
+ case "grep":
329
+ return ["grep", pattern, path, include ? `include ${include}` : undefined]
330
+ .filter(Boolean)
331
+ .join(" ");
332
+ case "find":
333
+ return ["find", path, pattern].filter(Boolean).join(" ");
334
+ case "ls":
335
+ return path ? `ls ${path}` : "ls";
336
+ default: {
337
+ const args = Object.entries(input)
338
+ .slice(0, 3)
339
+ .map(([key, value]) => {
340
+ const formatted = compactValue(value, 80);
341
+ return formatted ? `${key}=${formatted}` : undefined;
342
+ })
343
+ .filter(Boolean)
344
+ .join(" ");
345
+ return args ? `${toolName} ${args}` : toolName;
346
+ }
347
+ }
348
+ }
349
+
350
+ function compactToolResultText(text: string | undefined, isError: boolean): string | undefined {
351
+ if (!text) return undefined;
352
+ const compacted = compactText(text);
353
+ const maxChars = isError ? MAX_TOOL_RESULT_ERROR_CHARS : MAX_TOOL_RESULT_OK_CHARS;
354
+ return isError ? truncateText(compacted, maxChars) : tailText(compacted, maxChars);
355
+ }
356
+
327
357
  function extractTextContent(content: readonly unknown[] | undefined): string | undefined {
328
358
  if (!content) return undefined;
329
359
  const text = content
@@ -336,12 +366,8 @@ function extractTextContent(content: readonly unknown[] | undefined): string | u
336
366
  })
337
367
  .filter((part): part is string => Boolean(part))
338
368
  .join("\n");
339
- return text.length > 0 ? text : undefined;
340
- }
341
-
342
- function prefixed(icon: string, text: string, preset: IconPreset): string {
343
- if (preset === "plain") return `${icon}:${text}`;
344
- return `${icon} ${text}`;
369
+ const compacted = compactText(text);
370
+ return compacted.length > 0 ? compacted : undefined;
345
371
  }
346
372
 
347
373
  class TldrFactCollector {
@@ -354,7 +380,11 @@ class TldrFactCollector {
354
380
  }
355
381
 
356
382
  recordUserMessage(prompt: string): TldrActivity {
357
- return this.addActivity("user_message", "immediate", `User message: ${prompt}`);
383
+ return this.addActivity(
384
+ "user_message",
385
+ "immediate",
386
+ `user: ${truncateText(compactText(prompt), MAX_USER_TEXT_CHARS)}`,
387
+ );
358
388
  }
359
389
 
360
390
  recordAssistantUpdate(message: unknown): TldrActivity | undefined {
@@ -363,27 +393,38 @@ class TldrFactCollector {
363
393
  if (record.role !== "assistant") return undefined;
364
394
  const text = extractTextContent(record.content as readonly unknown[] | undefined);
365
395
  return text
366
- ? this.addActivity("assistant_update", "normal", `Assistant update: ${text}`)
396
+ ? this.addActivity(
397
+ "assistant_update",
398
+ "normal",
399
+ `assistant: ${tailText(text, MAX_ASSISTANT_UPDATE_CHARS)}`,
400
+ )
367
401
  : undefined;
368
402
  }
369
403
 
370
- recordToolCall(event: { toolName: string }): TldrActivity {
371
- return this.addActivity("tool_call", "normal", `Tool started: ${event.toolName}`);
404
+ recordToolCall(event: { toolName: string; input?: Record<string, unknown> }): TldrActivity {
405
+ return this.addActivity(
406
+ "tool_call",
407
+ "normal",
408
+ `tool: ${compactToolInput(event.toolName, event.input)}`,
409
+ );
372
410
  }
373
411
 
374
412
  recordToolResult(event: {
375
413
  toolName: string;
414
+ input?: Record<string, unknown>;
376
415
  isError?: boolean;
377
416
  content?: readonly unknown[];
378
417
  }): TldrActivity {
379
- const resultText = extractTextContent(event.content);
380
- const status = event.isError ? "error" : "ok";
418
+ const isError = Boolean(event.isError);
419
+ const resultText = compactToolResultText(extractTextContent(event.content), isError);
420
+ const status = isError ? "error" : "ok";
421
+ const tool = compactToolInput(event.toolName, event.input);
381
422
  return this.addActivity(
382
423
  "tool_result",
383
424
  "normal",
384
425
  resultText
385
- ? `Tool finished: ${event.toolName} (${status})\n${resultText}`
386
- : `Tool finished: ${event.toolName} (${status})`,
426
+ ? `result: ${tool} ${status}; ${resultText}`
427
+ : `result: ${tool} ${status}`,
387
428
  );
388
429
  }
389
430
 
@@ -399,12 +440,12 @@ class TldrFactCollector {
399
440
  if (stopReason === "stop") {
400
441
  const finalText = extractTextContent(record.content as readonly unknown[] | undefined);
401
442
  if (!finalText) return "emptyFinalStop";
402
- text = `Assistant final response: ${finalText}`;
443
+ text = `final: ${truncateText(finalText, MAX_FINAL_TEXT_CHARS)}`;
403
444
  } else if (stopReason) {
404
- const error = typeof record.errorMessage === "string" ? record.errorMessage : undefined;
445
+ const error = typeof record.errorMessage === "string" ? compactText(record.errorMessage) : undefined;
405
446
  text = error
406
- ? `Assistant finished with ${stopReason}: ${error}`
407
- : `Assistant finished with ${stopReason}`;
447
+ ? `final: ${stopReason}; ${truncateText(error, MAX_FINAL_TEXT_CHARS)}`
448
+ : `final: ${stopReason}`;
408
449
  }
409
450
 
410
451
  return text
@@ -501,13 +542,21 @@ class FooterTldrEngine {
501
542
  if (activity) this.enqueue(ctx, activity);
502
543
  }
503
544
 
504
- recordToolCall(ctx: ExtensionContext, event: { toolName: string }): void {
545
+ recordToolCall(
546
+ ctx: ExtensionContext,
547
+ event: { toolName: string; input?: Record<string, unknown> },
548
+ ): void {
505
549
  this.enqueue(ctx, this.facts.recordToolCall(event));
506
550
  }
507
551
 
508
552
  recordToolResult(
509
553
  ctx: ExtensionContext,
510
- event: { toolName: string; isError?: boolean; content?: readonly unknown[] },
554
+ event: {
555
+ toolName: string;
556
+ input?: Record<string, unknown>;
557
+ isError?: boolean;
558
+ content?: readonly unknown[];
559
+ },
511
560
  ): void {
512
561
  this.enqueue(ctx, this.facts.recordToolResult(event));
513
562
  }
@@ -746,7 +795,9 @@ Summarize only activity up to the requested index.
746
795
  If context is sparse, still summarize the available activity.
747
796
  Never ask for more information or say there is not enough context.
748
797
  Return one concise status fragment under ${TLDR_TARGET_SUMMARY_CHARS} characters.
749
- Use third person. Do not address the user.
798
+ Omit subjects like "the agent" or "it".
799
+ Prefer verb + direct object. Include outcome only if important.
800
+ Do not address the user.
750
801
  Plain text only; no markdown, JSON, code, bullets, quotes, or file/tool names.
751
802
  ${tenseInstruction}`;
752
803
  }
@@ -760,7 +811,7 @@ function previousCheckpointLines(checkpoints: readonly TldrCheckpoint[]): string
760
811
  }
761
812
 
762
813
  function formatRawActivity(activity: TldrActivity): string {
763
- return `[${activity.index}] ${activity.activityType}: ${activity.text}`;
814
+ return `[${activity.index}] ${activity.text}`;
764
815
  }
765
816
 
766
817
  function shouldShowStatus(key: string, filter: StatusFilter): boolean {
@@ -1078,8 +1129,6 @@ export default function (pi: ExtensionAPI) {
1078
1129
  if (!ctx.hasUI) return;
1079
1130
 
1080
1131
  const visibleSegments = parseSegments();
1081
- const iconPreset = parseIconPreset();
1082
- const icons = ICON_PRESETS[iconPreset];
1083
1132
  const { warningThreshold, errorThreshold } = parseThresholds();
1084
1133
 
1085
1134
  ctx.ui.setFooter((tui, theme, footerData) => {
@@ -1111,28 +1160,14 @@ export default function (pi: ExtensionAPI) {
1111
1160
  const tldrText = tldr.text();
1112
1161
 
1113
1162
  const segmentRenderers: Record<SegmentName, string | null> = {
1114
- model: theme.fg(
1115
- "accent",
1116
- prefixed(icons.model, modelName, iconPreset),
1117
- ),
1118
- thinking: theme.fg(
1119
- thinkingColor(thinkingLevel),
1120
- prefixed(icons.thinking, `think:${thinkingLevel}`, iconPreset),
1121
- ),
1122
- context: theme.fg(
1123
- contextSegmentColor,
1124
- prefixed(icons.context, contextText, iconPreset),
1125
- ),
1163
+ model: theme.fg("accent", modelName),
1164
+ thinking: theme.fg(thinkingColor(thinkingLevel), `think:${thinkingLevel}`),
1165
+ context: theme.fg(contextSegmentColor, contextText),
1126
1166
  tldr: tldrText ? theme.fg("muted", tldrText) : null,
1127
- extensions: extensionStatuses
1128
- ? theme.fg(
1129
- "muted",
1130
- prefixed(icons.extensions, extensionStatuses, iconPreset),
1131
- )
1132
- : null,
1167
+ extensions: extensionStatuses ? theme.fg("muted", extensionStatuses) : null,
1133
1168
  };
1134
1169
 
1135
- const separator = ` ${theme.fg("dim", icons.separator)} `;
1170
+ const separator = ` ${theme.fg("dim", SEGMENT_SEPARATOR)} `;
1136
1171
  const line = visibleSegments
1137
1172
  .map((segment) => segmentRenderers[segment])
1138
1173
  .filter((segment): segment is string => segment !== null)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-bar",
3
- "version": "0.3.13",
4
- "description": "Never accidentally run Opus on a typo. Pi footer/statusline showing active model, thinking level, context usage, live TLDR, and extension statuses.",
3
+ "version": "0.3.15",
4
+ "description": "Pi footer/statusline showing active model, thinking level, context pressure, live TLDR, and extension statuses.",
5
5
  "type": "module",
6
6
  "keywords": [
7
7
  "pi-package",
@@ -51,6 +51,6 @@
51
51
  "extensions": [
52
52
  "./extensions/status-footer.ts"
53
53
  ],
54
- "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.13/assets/screenshot.png"
54
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.15/assets/screenshot.png"
55
55
  }
56
56
  }