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 +3 -26
- package/extensions/status-footer.ts +125 -92
- package/package.json +2 -2
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
|
-
|
|
9
|
+
claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Inspecting package structure ❯ plan:active
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-

|
|
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
|
-
*
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
380
|
-
const
|
|
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
|
-
? `
|
|
386
|
-
: `
|
|
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 = `
|
|
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
|
-
? `
|
|
407
|
-
: `
|
|
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(
|
|
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: {
|
|
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.
|
|
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
|
-
|
|
1118
|
-
|
|
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",
|
|
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.
|
|
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.
|
|
54
|
+
"image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.15/assets/screenshot.png"
|
|
55
55
|
}
|
|
56
56
|
}
|