pi-bar 0.3.22 → 0.3.24
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
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Inspecting package structure ❯ plan:active
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-

|
|
13
|
+

|
|
14
|
+

|
|
13
15
|
|
|
14
16
|
## Why use it?
|
|
15
17
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -88,6 +88,14 @@ const TLDR_MAX_TOKENS = 120;
|
|
|
88
88
|
const TLDR_REQUEST_TIMEOUT_MS = 2_000;
|
|
89
89
|
const TLDR_DISPLAY_UPDATE_INTERVAL_MS = 1_200;
|
|
90
90
|
const TLDR_TARGET_SUMMARY_CHARS = 60;
|
|
91
|
+
// Burst debouncing on the generation side: coalesce rapid-fire activities into
|
|
92
|
+
// one model call. Quiet window catches short bursts; max wait surfaces progress
|
|
93
|
+
// during continuous activity.
|
|
94
|
+
const NORMAL_CHECKPOINT_QUIET_MS = 700;
|
|
95
|
+
const NORMAL_CHECKPOINT_MAX_WAIT_MS = 2_500;
|
|
96
|
+
// Defensive ceiling: even if the model ignores the length instruction, never
|
|
97
|
+
// blast a large payload into the footer.
|
|
98
|
+
const MAX_SAFE_TLDR_CHARS = 240;
|
|
91
99
|
const CONFIG_PATH =
|
|
92
100
|
process.env.PI_BAR_CONFIG ?? join(homedir(), ".pi", "agent", "pi-bar.json");
|
|
93
101
|
|
|
@@ -236,11 +244,13 @@ function resolveTldrModelPreference(cwd: string): TldrModelPreference | undefine
|
|
|
236
244
|
return globalModel ? parseTldrModelSpec(globalModel) : undefined;
|
|
237
245
|
}
|
|
238
246
|
|
|
247
|
+
// Fallback order optimizes for low latency + low cost first. The TLDR path does
|
|
248
|
+
// not need reasoning quality; codex/gpt-5.4-mini is fastest in practice.
|
|
239
249
|
const FAST_TLDR_MODELS: readonly TldrModelPreference[] = [
|
|
240
|
-
{ provider: "anthropic", id: "claude-haiku-4-5" },
|
|
241
|
-
{ provider: "anthropic", id: "claude-haiku-4-5-20251001" },
|
|
242
250
|
{ provider: "openai-codex", id: "gpt-5.4-mini" },
|
|
243
251
|
{ provider: "openai-codex", id: "gpt-5.3-codex-spark" },
|
|
252
|
+
{ provider: "anthropic", id: "claude-haiku-4-5" },
|
|
253
|
+
{ provider: "anthropic", id: "claude-haiku-4-5-20251001" },
|
|
244
254
|
];
|
|
245
255
|
|
|
246
256
|
function formatTldrModelKey(model: TldrModelPreference): string {
|
|
@@ -281,8 +291,19 @@ async function getFastTldrModelAuth(
|
|
|
281
291
|
}
|
|
282
292
|
|
|
283
293
|
function truncateText(text: string, maxChars: number): string {
|
|
284
|
-
if (
|
|
285
|
-
|
|
294
|
+
if (maxChars <= 0) return "";
|
|
295
|
+
const chars = Array.from(text);
|
|
296
|
+
if (chars.length <= maxChars) return text;
|
|
297
|
+
if (maxChars === 1) return "…";
|
|
298
|
+
|
|
299
|
+
// Head+tail preserves both "what started" and "how it ended". Most useful for
|
|
300
|
+
// long bash/tool outputs where the final lines carry the outcome.
|
|
301
|
+
const retainedChars = maxChars - 1;
|
|
302
|
+
const headLength = Math.ceil(retainedChars / 2);
|
|
303
|
+
const tailLength = Math.floor(retainedChars / 2);
|
|
304
|
+
const head = chars.slice(0, headLength).join("");
|
|
305
|
+
const tail = tailLength > 0 ? chars.slice(-tailLength).join("") : "";
|
|
306
|
+
return `${head}…${tail}`;
|
|
286
307
|
}
|
|
287
308
|
|
|
288
309
|
function tailText(text: string, maxChars: number): string {
|
|
@@ -604,6 +625,12 @@ class FooterTldrEngine {
|
|
|
604
625
|
private displayTimer?: ReturnType<typeof setTimeout>;
|
|
605
626
|
private abortController?: AbortController;
|
|
606
627
|
private currentText: string | null = null;
|
|
628
|
+
// Normal-priority bursts wait for a quiet window before triggering a model
|
|
629
|
+
// call. Pi often emits many rapid activities (e.g. streaming reads); without
|
|
630
|
+
// debouncing we would hammer the TLDR model for intermediate states.
|
|
631
|
+
private pendingNormalCheckpoint?: TldrCheckpointJob;
|
|
632
|
+
private normalCheckpointTimer?: ReturnType<typeof setTimeout>;
|
|
633
|
+
private normalCheckpointBurstStartedAt?: number;
|
|
607
634
|
|
|
608
635
|
constructor(private readonly requestRender: () => void) {}
|
|
609
636
|
|
|
@@ -681,12 +708,65 @@ class FooterTldrEngine {
|
|
|
681
708
|
private cancelWork(): void {
|
|
682
709
|
if (this.displayTimer) clearTimeout(this.displayTimer);
|
|
683
710
|
this.displayTimer = undefined;
|
|
711
|
+
this.clearPendingNormalCheckpoint();
|
|
684
712
|
this.checkpointQueue.splice(0);
|
|
685
713
|
this.inFlightCheckpoint = undefined;
|
|
686
714
|
this.abortController?.abort();
|
|
687
715
|
this.abortController = undefined;
|
|
688
716
|
}
|
|
689
717
|
|
|
718
|
+
private clearNormalCheckpointTimer(): void {
|
|
719
|
+
if (!this.normalCheckpointTimer) return;
|
|
720
|
+
clearTimeout(this.normalCheckpointTimer);
|
|
721
|
+
this.normalCheckpointTimer = undefined;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
private clearPendingNormalCheckpoint(): void {
|
|
725
|
+
this.clearNormalCheckpointTimer();
|
|
726
|
+
this.pendingNormalCheckpoint = undefined;
|
|
727
|
+
this.normalCheckpointBurstStartedAt = undefined;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private scheduleNormalCheckpoint(
|
|
731
|
+
ctx: ExtensionContext,
|
|
732
|
+
job: TldrCheckpointJob,
|
|
733
|
+
): void {
|
|
734
|
+
this.pendingNormalCheckpoint = job;
|
|
735
|
+
this.normalCheckpointBurstStartedAt ??= performance.now();
|
|
736
|
+
this.clearNormalCheckpointTimer();
|
|
737
|
+
|
|
738
|
+
const elapsedSinceBurstStarted =
|
|
739
|
+
performance.now() - this.normalCheckpointBurstStartedAt;
|
|
740
|
+
const maxWaitRemainingMs =
|
|
741
|
+
NORMAL_CHECKPOINT_MAX_WAIT_MS - elapsedSinceBurstStarted;
|
|
742
|
+
const delayMs = Math.max(
|
|
743
|
+
0,
|
|
744
|
+
Math.min(NORMAL_CHECKPOINT_QUIET_MS, maxWaitRemainingMs),
|
|
745
|
+
);
|
|
746
|
+
|
|
747
|
+
if (delayMs === 0) {
|
|
748
|
+
this.flushPendingNormalCheckpoint(ctx);
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
this.normalCheckpointTimer = setTimeout(() => {
|
|
753
|
+
this.normalCheckpointTimer = undefined;
|
|
754
|
+
this.flushPendingNormalCheckpoint(ctx);
|
|
755
|
+
}, delayMs);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private flushPendingNormalCheckpoint(ctx: ExtensionContext): void {
|
|
759
|
+
const job = this.pendingNormalCheckpoint;
|
|
760
|
+
this.clearPendingNormalCheckpoint();
|
|
761
|
+
if (!job || job.runId !== this.runId) return;
|
|
762
|
+
|
|
763
|
+
this.checkpointQueue = this.checkpointQueue.filter(
|
|
764
|
+
(queued) => queued.displayPriority !== "normal",
|
|
765
|
+
);
|
|
766
|
+
this.checkpointQueue.push(job);
|
|
767
|
+
this.pump(ctx);
|
|
768
|
+
}
|
|
769
|
+
|
|
690
770
|
private enqueue(ctx: ExtensionContext, activity: TldrActivity): void {
|
|
691
771
|
if (!ctx.hasUI) return;
|
|
692
772
|
const job = {
|
|
@@ -697,12 +777,18 @@ class FooterTldrEngine {
|
|
|
697
777
|
|
|
698
778
|
if (job.displayPriority === "immediate") {
|
|
699
779
|
this.clearPendingDisplay();
|
|
780
|
+
this.clearPendingNormalCheckpoint();
|
|
700
781
|
this.lastRenderedText = "";
|
|
701
782
|
this.checkpointQueue.splice(0);
|
|
702
783
|
this.abortInFlightCheckpoint();
|
|
703
784
|
this.checkpointQueue.push(job);
|
|
704
|
-
|
|
785
|
+
this.pump(ctx);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (job.displayPriority === "final") {
|
|
705
790
|
this.clearPendingDisplay();
|
|
791
|
+
this.clearPendingNormalCheckpoint();
|
|
706
792
|
this.checkpointQueue = this.checkpointQueue.filter(
|
|
707
793
|
(queued) => queued.displayPriority !== "normal",
|
|
708
794
|
);
|
|
@@ -710,14 +796,14 @@ class FooterTldrEngine {
|
|
|
710
796
|
this.abortInFlightCheckpoint();
|
|
711
797
|
}
|
|
712
798
|
this.checkpointQueue.push(job);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
(queued) => queued.displayPriority !== "normal",
|
|
716
|
-
);
|
|
717
|
-
this.checkpointQueue.push(job);
|
|
799
|
+
this.pump(ctx);
|
|
800
|
+
return;
|
|
718
801
|
}
|
|
719
802
|
|
|
720
|
-
|
|
803
|
+
// Normal-priority: debounce into a single coalesced job. The latest job
|
|
804
|
+
// always wins because raw activity since the last accepted checkpoint is
|
|
805
|
+
// what the model already sees, regardless of which job triggers the call.
|
|
806
|
+
this.scheduleNormalCheckpoint(ctx, job);
|
|
721
807
|
}
|
|
722
808
|
|
|
723
809
|
private clearPendingDisplay(): void {
|
|
@@ -883,7 +969,13 @@ function checkpointSystemPrompt(job: TldrCheckpointJob): string {
|
|
|
883
969
|
job.displayPriority === "final"
|
|
884
970
|
? "Start with a past-tense verb."
|
|
885
971
|
: "Start with a present-tense -ing verb.";
|
|
886
|
-
|
|
972
|
+
// Frame the model as a human developer describing visible work, not an agent
|
|
973
|
+
// summarizing its own mechanics. Avoids leaks like tool names, prompt labels,
|
|
974
|
+
// or our internal scaffolding (activity indexes, prior checkpoints).
|
|
975
|
+
return `Write one plain-English TLDR for a Pi coding agent.
|
|
976
|
+
Describe the work progress as if a human developer were doing it.
|
|
977
|
+
Focus on the task activity and current outcome, not agent mechanics.
|
|
978
|
+
Do not mention tools, tool calls, prompts, messages, model output, or implementation details.
|
|
887
979
|
Use the prior TLDRs for context and the new activity for the update.
|
|
888
980
|
Summarize the current state of work; do not narrate the history.
|
|
889
981
|
If context is sparse, still summarize the available activity.
|
|
@@ -892,7 +984,6 @@ Return one concise status fragment under ${TLDR_TARGET_SUMMARY_CHARS} characters
|
|
|
892
984
|
Omit subjects like "the agent" or "it".
|
|
893
985
|
Prefer verb + direct object. Include outcome only if important.
|
|
894
986
|
Do not address the user.
|
|
895
|
-
Do not mention activity numbers, indexes, checkpoints, prior TLDRs, or phrases like "through activity".
|
|
896
987
|
Output only the status fragment itself. No prefixes, labels, bullets, or quotes.
|
|
897
988
|
Plain text only; no markdown, JSON, code, file paths, or tool names.
|
|
898
989
|
${tenseInstruction}`;
|
|
@@ -910,20 +1001,134 @@ function formatRawActivity(activity: TldrActivity): string {
|
|
|
910
1001
|
return `- ${activity.text}`;
|
|
911
1002
|
}
|
|
912
1003
|
|
|
1004
|
+
// Strips ANSI/CSI/OSC/DCS/SOS/PM/APC escape sequences from model output before
|
|
1005
|
+
// it lands in the terminal. The TLDR text is model-controlled and renders into
|
|
1006
|
+
// the footer; the system prompt asking for plain text is not a security
|
|
1007
|
+
// boundary, so we defensively remove terminal controls here.
|
|
1008
|
+
const ESC_BYTE = 0x1b;
|
|
1009
|
+
const BEL_BYTE = 0x07;
|
|
1010
|
+
const ST_BYTE = 0x9c;
|
|
1011
|
+
|
|
1012
|
+
function isControlCharacter(code: number): boolean {
|
|
1013
|
+
return (code >= 0x00 && code <= 0x1f) || (code >= 0x7f && code <= 0x9f);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function isWhitespaceControl(code: number): boolean {
|
|
1017
|
+
return (
|
|
1018
|
+
code === 0x09 ||
|
|
1019
|
+
code === 0x0a ||
|
|
1020
|
+
code === 0x0b ||
|
|
1021
|
+
code === 0x0c ||
|
|
1022
|
+
code === 0x0d
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function skipCsiSequence(text: string, startIndex: number): number {
|
|
1027
|
+
for (let index = startIndex; index < text.length; index++) {
|
|
1028
|
+
const code = text.charCodeAt(index);
|
|
1029
|
+
if (code >= 0x40 && code <= 0x7e) return index + 1;
|
|
1030
|
+
}
|
|
1031
|
+
return text.length;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
function skipStringControl(text: string, startIndex: number): number {
|
|
1035
|
+
for (let index = startIndex; index < text.length; index++) {
|
|
1036
|
+
const code = text.charCodeAt(index);
|
|
1037
|
+
if (code === BEL_BYTE || code === ST_BYTE) return index + 1;
|
|
1038
|
+
if (code === ESC_BYTE && text.charCodeAt(index + 1) === 0x5c) {
|
|
1039
|
+
return index + 2;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return text.length;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function skipEscapeSequence(text: string, escapeIndex: number): number {
|
|
1046
|
+
const nextCode = text.charCodeAt(escapeIndex + 1);
|
|
1047
|
+
if (Number.isNaN(nextCode)) return escapeIndex + 1;
|
|
1048
|
+
|
|
1049
|
+
switch (nextCode) {
|
|
1050
|
+
case 0x5b: // CSI: ESC [
|
|
1051
|
+
return skipCsiSequence(text, escapeIndex + 2);
|
|
1052
|
+
case 0x5d: // OSC: ESC ]
|
|
1053
|
+
case 0x50: // DCS: ESC P
|
|
1054
|
+
case 0x58: // SOS: ESC X
|
|
1055
|
+
case 0x5e: // PM: ESC ^
|
|
1056
|
+
case 0x5f: // APC: ESC _
|
|
1057
|
+
return skipStringControl(text, escapeIndex + 2);
|
|
1058
|
+
default:
|
|
1059
|
+
if (
|
|
1060
|
+
nextCode === 0x20 ||
|
|
1061
|
+
nextCode === 0x23 ||
|
|
1062
|
+
nextCode === 0x25 ||
|
|
1063
|
+
(nextCode >= 0x28 && nextCode <= 0x2f)
|
|
1064
|
+
) {
|
|
1065
|
+
return Math.min(text.length, escapeIndex + 3);
|
|
1066
|
+
}
|
|
1067
|
+
return Math.min(text.length, escapeIndex + 2);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function stripTerminalControls(text: string): string {
|
|
1072
|
+
let stripped = "";
|
|
1073
|
+
for (let index = 0; index < text.length; ) {
|
|
1074
|
+
const code = text.charCodeAt(index);
|
|
1075
|
+
|
|
1076
|
+
if (code === ESC_BYTE) {
|
|
1077
|
+
index = skipEscapeSequence(text, index);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
if (code === 0x9b) {
|
|
1081
|
+
index = skipCsiSequence(text, index + 1);
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
if (
|
|
1085
|
+
code === 0x90 ||
|
|
1086
|
+
code === 0x98 ||
|
|
1087
|
+
code === 0x9d ||
|
|
1088
|
+
code === 0x9e ||
|
|
1089
|
+
code === 0x9f
|
|
1090
|
+
) {
|
|
1091
|
+
index = skipStringControl(text, index + 1);
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
if (isControlCharacter(code)) {
|
|
1095
|
+
if (isWhitespaceControl(code)) stripped += " ";
|
|
1096
|
+
index++;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
stripped += text[index];
|
|
1101
|
+
index++;
|
|
1102
|
+
}
|
|
1103
|
+
return stripped.replace(/\s+/gu, " ").trim();
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Second layer: strip leaked prompt scaffolding such as "Through activity 12:".
|
|
1107
|
+
// The system prompt forbids these prefixes, but model outputs sometimes still
|
|
1108
|
+
// parrot them; the regex is narrow enough not to clip natural starts like
|
|
1109
|
+
// "Activity slowed after retry" or "Checkpointing release state".
|
|
913
1110
|
const LEAKED_PREFIX_PATTERN =
|
|
914
1111
|
/^\s*(?:[-*•]\s*)?(?:(?:through\s+activity|activity|checkpoint)\s+\d+\s*[:.\-—–]\s*|(?:tldr|summary)\s*[:.\-—–]\s*)+/i;
|
|
915
|
-
|
|
916
1112
|
const LEADING_PUNCT_PATTERN = /^[\s\-—–•*:#.,;]+/;
|
|
917
1113
|
|
|
918
|
-
function
|
|
919
|
-
let cleaned = text
|
|
1114
|
+
function stripLeakedScaffolding(text: string): string {
|
|
1115
|
+
let cleaned = text;
|
|
920
1116
|
let previous: string;
|
|
921
1117
|
do {
|
|
922
1118
|
previous = cleaned;
|
|
923
1119
|
cleaned = cleaned.replace(LEAKED_PREFIX_PATTERN, "").trim();
|
|
924
1120
|
cleaned = cleaned.replace(LEADING_PUNCT_PATTERN, "").trim();
|
|
925
1121
|
} while (cleaned !== previous && cleaned.length > 0);
|
|
926
|
-
return cleaned
|
|
1122
|
+
return cleaned;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function sanitizeTldrText(
|
|
1126
|
+
text: string,
|
|
1127
|
+
maxChars = MAX_SAFE_TLDR_CHARS,
|
|
1128
|
+
): string {
|
|
1129
|
+
const stripped = stripTerminalControls(text);
|
|
1130
|
+
const cleaned = stripLeakedScaffolding(stripped) || stripped;
|
|
1131
|
+
return truncateText(cleaned, maxChars);
|
|
927
1132
|
}
|
|
928
1133
|
|
|
929
1134
|
function shouldShowStatus(key: string, filter: StatusFilter): boolean {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bar",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
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.24/assets/screenshot-yellow.png"
|
|
55
55
|
}
|
|
56
56
|
}
|
package/assets/screenshot.png
DELETED
|
Binary file
|