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
- ![pi-bar screenshot, three rows showing green, yellow, and red context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.22/assets/screenshot.png)
12
+ ![pi-bar with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.24/assets/screenshot-green.png)
13
+ ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.24/assets/screenshot-yellow.png)
14
+ ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.24/assets/screenshot-red.png)
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 (text.length <= maxChars) return text;
285
- return `${text.slice(0, maxChars - 1)}…`;
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
- } else if (job.displayPriority === "final") {
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
- } else {
714
- this.checkpointQueue = this.checkpointQueue.filter(
715
- (queued) => queued.displayPriority !== "normal",
716
- );
717
- this.checkpointQueue.push(job);
799
+ this.pump(ctx);
800
+ return;
718
801
  }
719
802
 
720
- this.pump(ctx);
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
- return `Write one plain-English TLDR for a terminal coding agent.
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 sanitizeTldrText(text: string): string {
919
- let cleaned = text.trim();
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 || text.trim();
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.22",
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.22/assets/screenshot.png"
54
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.24/assets/screenshot-yellow.png"
55
55
  }
56
56
  }
Binary file