pi-bar 0.3.14 → 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
@@ -6,10 +6,10 @@
6
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.14/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
 
@@ -48,7 +48,7 @@ Allowed segments are `model`, `thinking`, `context`, `tldr`, and `extensions`. T
48
48
 
49
49
  ### Configure live TLDR
50
50
 
51
- pi-bar shows a concise live TLDR of the current task in the footer. 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.
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
53
  Override the TLDR model with:
54
54
 
@@ -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.
@@ -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
  }
@@ -762,7 +811,7 @@ function previousCheckpointLines(checkpoints: readonly TldrCheckpoint[]): string
762
811
  }
763
812
 
764
813
  function formatRawActivity(activity: TldrActivity): string {
765
- return `[${activity.index}] ${activity.activityType}: ${activity.text}`;
814
+ return `[${activity.index}] ${activity.text}`;
766
815
  }
767
816
 
768
817
  function shouldShowStatus(key: string, filter: StatusFilter): boolean {
@@ -1080,8 +1129,6 @@ export default function (pi: ExtensionAPI) {
1080
1129
  if (!ctx.hasUI) return;
1081
1130
 
1082
1131
  const visibleSegments = parseSegments();
1083
- const iconPreset = parseIconPreset();
1084
- const icons = ICON_PRESETS[iconPreset];
1085
1132
  const { warningThreshold, errorThreshold } = parseThresholds();
1086
1133
 
1087
1134
  ctx.ui.setFooter((tui, theme, footerData) => {
@@ -1113,28 +1160,14 @@ export default function (pi: ExtensionAPI) {
1113
1160
  const tldrText = tldr.text();
1114
1161
 
1115
1162
  const segmentRenderers: Record<SegmentName, string | null> = {
1116
- model: theme.fg(
1117
- "accent",
1118
- prefixed(icons.model, modelName, iconPreset),
1119
- ),
1120
- thinking: theme.fg(
1121
- thinkingColor(thinkingLevel),
1122
- prefixed(icons.thinking, `think:${thinkingLevel}`, iconPreset),
1123
- ),
1124
- context: theme.fg(
1125
- contextSegmentColor,
1126
- prefixed(icons.context, contextText, iconPreset),
1127
- ),
1163
+ model: theme.fg("accent", modelName),
1164
+ thinking: theme.fg(thinkingColor(thinkingLevel), `think:${thinkingLevel}`),
1165
+ context: theme.fg(contextSegmentColor, contextText),
1128
1166
  tldr: tldrText ? theme.fg("muted", tldrText) : null,
1129
- extensions: extensionStatuses
1130
- ? theme.fg(
1131
- "muted",
1132
- prefixed(icons.extensions, extensionStatuses, iconPreset),
1133
- )
1134
- : null,
1167
+ extensions: extensionStatuses ? theme.fg("muted", extensionStatuses) : null,
1135
1168
  };
1136
1169
 
1137
- const separator = ` ${theme.fg("dim", icons.separator)} `;
1170
+ const separator = ` ${theme.fg("dim", SEGMENT_SEPARATOR)} `;
1138
1171
  const line = visibleSegments
1139
1172
  .map((segment) => segmentRenderers[segment])
1140
1173
  .filter((segment): segment is string => segment !== null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bar",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "Pi footer/statusline showing active model, thinking level, context pressure, live TLDR, and extension statuses.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -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.14/assets/screenshot.png"
54
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.15/assets/screenshot.png"
55
55
  }
56
56
  }