pi-bar 0.3.14 → 0.3.17

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.17/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 recent prompt, assistant, and deterministic code-compressed tool facts (for example `bash ... ok`, `grep ... 4 matches`, or `edit file`). 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 = 160;
81
+ const MAX_TOOL_RESULT_ERROR_CHARS = 320;
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,115 @@ 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 usefulLineCount(text: string): number {
351
+ return text
352
+ .split("\n")
353
+ .map((line) => line.trim())
354
+ .filter(Boolean).length;
355
+ }
356
+
357
+ function errorSummary(text: string | undefined): string | undefined {
358
+ if (!text) return undefined;
359
+ const compacted = compactText(text);
360
+ const exitMatch = compacted.match(/Command exited with code \d+/);
361
+ if (exitMatch) return truncateText(exitMatch[0], MAX_TOOL_RESULT_ERROR_CHARS);
362
+ return truncateText(compacted, MAX_TOOL_RESULT_ERROR_CHARS);
363
+ }
364
+
365
+ function summarizeToolResult(
366
+ toolName: string,
367
+ input: Record<string, unknown> | undefined,
368
+ text: string | undefined,
369
+ isError: boolean,
370
+ ): string | undefined {
371
+ if (isError) return errorSummary(text);
372
+
373
+ const lineCount = text ? usefulLineCount(text) : 0;
374
+ const path = compactValue(input?.path ?? input?.filePath ?? input?.cwd, 120);
375
+
376
+ switch (toolName) {
377
+ case "bash":
378
+ return undefined;
379
+ case "read":
380
+ return path ? `${lineCount || "file"} lines from ${path}` : undefined;
381
+ case "edit":
382
+ return path ? `edited ${path}` : undefined;
383
+ case "write":
384
+ return path ? `wrote ${path}` : undefined;
385
+ case "grep":
386
+ return lineCount ? `${lineCount} matches` : "no matches";
387
+ case "find":
388
+ return lineCount ? `${lineCount} paths` : "no paths";
389
+ case "ls":
390
+ return lineCount ? `${lineCount} entries` : undefined;
391
+ default:
392
+ return lineCount ? `${lineCount} output lines` : undefined;
393
+ }
394
+ }
395
+
327
396
  function extractTextContent(content: readonly unknown[] | undefined): string | undefined {
328
397
  if (!content) return undefined;
329
398
  const text = content
@@ -336,12 +405,8 @@ function extractTextContent(content: readonly unknown[] | undefined): string | u
336
405
  })
337
406
  .filter((part): part is string => Boolean(part))
338
407
  .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}`;
408
+ const compacted = compactText(text);
409
+ return compacted.length > 0 ? compacted : undefined;
345
410
  }
346
411
 
347
412
  class TldrFactCollector {
@@ -354,7 +419,11 @@ class TldrFactCollector {
354
419
  }
355
420
 
356
421
  recordUserMessage(prompt: string): TldrActivity {
357
- return this.addActivity("user_message", "immediate", `User message: ${prompt}`);
422
+ return this.addActivity(
423
+ "user_message",
424
+ "immediate",
425
+ `user: ${truncateText(compactText(prompt), MAX_USER_TEXT_CHARS)}`,
426
+ );
358
427
  }
359
428
 
360
429
  recordAssistantUpdate(message: unknown): TldrActivity | undefined {
@@ -363,27 +432,44 @@ class TldrFactCollector {
363
432
  if (record.role !== "assistant") return undefined;
364
433
  const text = extractTextContent(record.content as readonly unknown[] | undefined);
365
434
  return text
366
- ? this.addActivity("assistant_update", "normal", `Assistant update: ${text}`)
435
+ ? this.addActivity(
436
+ "assistant_update",
437
+ "normal",
438
+ `assistant: ${tailText(text, MAX_ASSISTANT_UPDATE_CHARS)}`,
439
+ )
367
440
  : undefined;
368
441
  }
369
442
 
370
- recordToolCall(event: { toolName: string }): TldrActivity {
371
- return this.addActivity("tool_call", "normal", `Tool started: ${event.toolName}`);
443
+ recordToolCall(event: { toolName: string; input?: Record<string, unknown> }): TldrActivity {
444
+ return this.addActivity(
445
+ "tool_call",
446
+ "normal",
447
+ `tool: ${compactToolInput(event.toolName, event.input)}`,
448
+ );
372
449
  }
373
450
 
374
451
  recordToolResult(event: {
375
452
  toolName: string;
453
+ input?: Record<string, unknown>;
376
454
  isError?: boolean;
377
455
  content?: readonly unknown[];
456
+ details?: unknown;
378
457
  }): TldrActivity {
379
- const resultText = extractTextContent(event.content);
380
- const status = event.isError ? "error" : "ok";
458
+ const isError = Boolean(event.isError);
459
+ const resultText = summarizeToolResult(
460
+ event.toolName,
461
+ event.input,
462
+ extractTextContent(event.content),
463
+ isError,
464
+ );
465
+ const status = isError ? "error" : "ok";
466
+ const tool = compactToolInput(event.toolName, event.input);
381
467
  return this.addActivity(
382
468
  "tool_result",
383
469
  "normal",
384
470
  resultText
385
- ? `Tool finished: ${event.toolName} (${status})\n${resultText}`
386
- : `Tool finished: ${event.toolName} (${status})`,
471
+ ? `result: ${tool} ${status}; ${resultText}`
472
+ : `result: ${tool} ${status}`,
387
473
  );
388
474
  }
389
475
 
@@ -399,12 +485,12 @@ class TldrFactCollector {
399
485
  if (stopReason === "stop") {
400
486
  const finalText = extractTextContent(record.content as readonly unknown[] | undefined);
401
487
  if (!finalText) return "emptyFinalStop";
402
- text = `Assistant final response: ${finalText}`;
488
+ text = `final: ${truncateText(finalText, MAX_FINAL_TEXT_CHARS)}`;
403
489
  } else if (stopReason) {
404
- const error = typeof record.errorMessage === "string" ? record.errorMessage : undefined;
490
+ const error = typeof record.errorMessage === "string" ? compactText(record.errorMessage) : undefined;
405
491
  text = error
406
- ? `Assistant finished with ${stopReason}: ${error}`
407
- : `Assistant finished with ${stopReason}`;
492
+ ? `final: ${stopReason}; ${truncateText(error, MAX_FINAL_TEXT_CHARS)}`
493
+ : `final: ${stopReason}`;
408
494
  }
409
495
 
410
496
  return text
@@ -501,13 +587,22 @@ class FooterTldrEngine {
501
587
  if (activity) this.enqueue(ctx, activity);
502
588
  }
503
589
 
504
- recordToolCall(ctx: ExtensionContext, event: { toolName: string }): void {
590
+ recordToolCall(
591
+ ctx: ExtensionContext,
592
+ event: { toolName: string; input?: Record<string, unknown> },
593
+ ): void {
505
594
  this.enqueue(ctx, this.facts.recordToolCall(event));
506
595
  }
507
596
 
508
597
  recordToolResult(
509
598
  ctx: ExtensionContext,
510
- event: { toolName: string; isError?: boolean; content?: readonly unknown[] },
599
+ event: {
600
+ toolName: string;
601
+ input?: Record<string, unknown>;
602
+ isError?: boolean;
603
+ content?: readonly unknown[];
604
+ details?: unknown;
605
+ },
511
606
  ): void {
512
607
  this.enqueue(ctx, this.facts.recordToolResult(event));
513
608
  }
@@ -762,7 +857,7 @@ function previousCheckpointLines(checkpoints: readonly TldrCheckpoint[]): string
762
857
  }
763
858
 
764
859
  function formatRawActivity(activity: TldrActivity): string {
765
- return `[${activity.index}] ${activity.activityType}: ${activity.text}`;
860
+ return `[${activity.index}] ${activity.text}`;
766
861
  }
767
862
 
768
863
  function shouldShowStatus(key: string, filter: StatusFilter): boolean {
@@ -1080,8 +1175,6 @@ export default function (pi: ExtensionAPI) {
1080
1175
  if (!ctx.hasUI) return;
1081
1176
 
1082
1177
  const visibleSegments = parseSegments();
1083
- const iconPreset = parseIconPreset();
1084
- const icons = ICON_PRESETS[iconPreset];
1085
1178
  const { warningThreshold, errorThreshold } = parseThresholds();
1086
1179
 
1087
1180
  ctx.ui.setFooter((tui, theme, footerData) => {
@@ -1113,28 +1206,14 @@ export default function (pi: ExtensionAPI) {
1113
1206
  const tldrText = tldr.text();
1114
1207
 
1115
1208
  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
- ),
1209
+ model: theme.fg("accent", modelName),
1210
+ thinking: theme.fg(thinkingColor(thinkingLevel), `think:${thinkingLevel}`),
1211
+ context: theme.fg(contextSegmentColor, contextText),
1128
1212
  tldr: tldrText ? theme.fg("muted", tldrText) : null,
1129
- extensions: extensionStatuses
1130
- ? theme.fg(
1131
- "muted",
1132
- prefixed(icons.extensions, extensionStatuses, iconPreset),
1133
- )
1134
- : null,
1213
+ extensions: extensionStatuses ? theme.fg("muted", extensionStatuses) : null,
1135
1214
  };
1136
1215
 
1137
- const separator = ` ${theme.fg("dim", icons.separator)} `;
1216
+ const separator = ` ${theme.fg("dim", SEGMENT_SEPARATOR)} `;
1138
1217
  const line = visibleSegments
1139
1218
  .map((segment) => segmentRenderers[segment])
1140
1219
  .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.17",
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.17/assets/screenshot.png"
55
55
  }
56
56
  }