pi-bar 0.3.26 → 0.3.28

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,9 +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 with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.26/assets/screenshot-green.png)
13
- ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.26/assets/screenshot-yellow.png)
14
- ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.26/assets/screenshot-red.png)
12
+ ![pi-bar with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.28/assets/screenshot-green.png)
13
+ ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.28/assets/screenshot-yellow.png)
14
+ ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.28/assets/screenshot-red.png)
15
15
 
16
16
  ## Why use it?
17
17
 
@@ -91,7 +91,9 @@ const TLDR_TARGET_SUMMARY_CHARS = 60;
91
91
  // Burst debouncing on the generation side: coalesce rapid-fire activities into
92
92
  // one model call. Quiet window catches short bursts; max wait surfaces progress
93
93
  // during continuous activity.
94
- const NORMAL_CHECKPOINT_QUIET_MS = 700;
94
+ // Quiet window long enough to let tool_result land before flushing, so the
95
+ // model summarizes the completed outcome instead of guessing from tool_call.
96
+ const NORMAL_CHECKPOINT_QUIET_MS = 1_500;
95
97
  const NORMAL_CHECKPOINT_MAX_WAIT_MS = 2_500;
96
98
  // Defensive ceiling: even if the model ignores the length instruction, never
97
99
  // blast a large payload into the footer.
@@ -650,7 +652,14 @@ class FooterTldrEngine {
650
652
  }
651
653
 
652
654
  recordUserMessage(ctx: ExtensionContext, prompt: string): void {
655
+ // A new user turn replaces the entire visible TLDR context, so wipe both
656
+ // the rendered text and the prior-turn checkpoint context that feeds the
657
+ // model. Carrying old checkpoints biased the next turn's phrasing toward
658
+ // the previous task.
653
659
  this.currentText = null;
660
+ this.lastRenderedText = "";
661
+ this.acceptedCheckpoints.splice(0);
662
+ this.latestAcceptedActivityIndex = 0;
654
663
  this.requestRender();
655
664
  this.enqueue(ctx, this.facts.recordUserMessage(prompt));
656
665
  }
@@ -661,10 +670,14 @@ class FooterTldrEngine {
661
670
  }
662
671
 
663
672
  recordToolCall(
664
- ctx: ExtensionContext,
673
+ _ctx: ExtensionContext,
665
674
  event: { toolName: string; input?: Record<string, unknown>; toolCallId?: string },
666
675
  ): void {
667
- this.enqueue(ctx, this.facts.recordToolCall(event));
676
+ // Record the fact so a subsequent checkpoint sees the tool was started,
677
+ // but DO NOT enqueue. tool_call alone has no outcome; flushing here
678
+ // before tool_result arrives makes the model hallucinate "completed
679
+ // successfully". The tool_result event triggers the actual checkpoint.
680
+ this.facts.recordToolCall(event);
668
681
  }
669
682
 
670
683
  recordToolResult(
@@ -689,9 +702,33 @@ class FooterTldrEngine {
689
702
  this.startFreshRun();
690
703
  return;
691
704
  }
705
+
706
+ // Failure/aborted finals have no narratable content. A model call here
707
+ // fabricates a reason; render a deterministic literal instead.
708
+ if (result.activityType === "assistant_failure") {
709
+ const stopReason = extractStopReason(message);
710
+ this.renderLiteralFinal(stopReason);
711
+ this.facts.resetConversation();
712
+ this.startFreshRun();
713
+ return;
714
+ }
715
+
692
716
  this.enqueue(ctx, result);
693
717
  }
694
718
 
719
+ private renderLiteralFinal(stopReason: string | undefined): void {
720
+ this.clearPendingDisplay();
721
+ this.clearPendingNormalCheckpoint();
722
+ this.checkpointQueue.splice(0);
723
+ this.abortInFlightCheckpoint();
724
+ const literal = stopReason === "aborted" ? "Aborted." : `Stopped: ${stopReason ?? "error"}.`;
725
+ this.currentText = literal;
726
+ this.lastRenderedText = literal;
727
+ this.lastRenderedActivityIndex = this.facts.latestActivityIndex();
728
+ this.lastDisplayAt = performance.now();
729
+ this.requestRender();
730
+ }
731
+
695
732
  private startFreshRun(): void {
696
733
  this.runId++;
697
734
  this.cancelWork();
@@ -930,6 +967,13 @@ class FooterTldrEngine {
930
967
  private renderCheckpoint(checkpoint: TldrCheckpoint): void {
931
968
  if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
932
969
  if (checkpoint.text === this.lastRenderedText) return;
970
+ // Skip near-duplicates: a burst of consecutive checkpoints often produces
971
+ // the same action fragment with only punctuation/casing differences. They
972
+ // flash visibly in the footer without adding information.
973
+ if (isNearDuplicateTldr(checkpoint.text, this.lastRenderedText)) {
974
+ this.lastRenderedActivityIndex = checkpoint.activityIndex;
975
+ return;
976
+ }
933
977
  this.lastRenderedActivityIndex = checkpoint.activityIndex;
934
978
  this.lastRenderedText = checkpoint.text;
935
979
  this.lastDisplayAt = performance.now();
@@ -965,17 +1009,54 @@ class FooterTldrEngine {
965
1009
  }
966
1010
 
967
1011
  function checkpointSystemPrompt(job: TldrCheckpointJob): string {
968
- const tenseInstruction =
969
- job.displayPriority === "final"
970
- ? "Start with a past-tense verb."
971
- : "Start with a present-tense -ing verb.";
1012
+ // Tense + exemplars vary by priority. Final TLDRs describe completed turns;
1013
+ // using -ing examples for final caused all final TLDRs to slip back into
1014
+ // present-progressive even though the literal instruction said past tense.
1015
+ // Immediate TLDRs cover a brand-new user request; they need a rephrase
1016
+ // directive instead of generic filler like "Continuing with task".
1017
+ let tenseInstruction: string;
1018
+ let goodExamples: string;
1019
+ if (job.displayPriority === "final") {
1020
+ tenseInstruction = "Start with a past-tense verb describing what was completed.";
1021
+ goodExamples = [
1022
+ "- Updated footer summary behavior",
1023
+ "- Investigated live TLDR regressions",
1024
+ "- Refined sanitizer for stray prefixes",
1025
+ "- Wrapped up extension release",
1026
+ ].join("\n");
1027
+ } else if (job.displayPriority === "immediate") {
1028
+ tenseInstruction =
1029
+ "Rephrase the user's new request as a concise present-progressive task clause. Do not write \"Continuing\", \"Investigating user input\", or any generic filler.";
1030
+ goodExamples = [
1031
+ "- Reviewing footer summary behavior",
1032
+ "- Investigating live TLDR regressions",
1033
+ "- Refining sanitizer for stray prefixes",
1034
+ "- Preparing extension release",
1035
+ ].join("\n");
1036
+ } else {
1037
+ tenseInstruction = "Start with a present-tense -ing verb describing current work.";
1038
+ goodExamples = [
1039
+ "- Reviewing footer summary behavior",
1040
+ "- Investigating live TLDR regressions",
1041
+ "- Refining sanitizer for stray prefixes",
1042
+ "- Wrapping up extension release",
1043
+ ].join("\n");
1044
+ }
1045
+
972
1046
  // 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).
1047
+ // summarizing its own mechanics. The banned phrases and few-shot examples
1048
+ // fix concrete regressions observed in backtests: tool-name verbs, success
1049
+ // suffixes, file path leaks, and markdown formatting.
975
1050
  return `Write one plain-English TLDR for a Pi coding agent.
976
1051
  Describe the work progress as if a human developer were doing it.
977
1052
  Focus on the task activity and current outcome, not agent mechanics.
978
1053
  Do not mention tools, tool calls, prompts, messages, model output, or implementation details.
1054
+ Do not start with tool-narration verbs such as Read, Reading, Grep, Listing, Counting, Extracting, Displaying, Editing, Writing, Running, Publishing, Capturing, Verifying, Validating, Checking, Confirming, Searching, Finding.
1055
+ Use human-developer verbs instead: Reviewing, Investigating, Exploring, Updating, Refining, Fixing, Implementing, Wrapping up, Bumping, Releasing.
1056
+ Do not use file paths, file extensions, code identifiers, package names, or version strings.
1057
+ Do not use backticks, asterisks, underscores, quotes, or any markdown formatting.
1058
+ Do not append filler suffixes such as "with success", "successfully", or "completed successfully".
1059
+ Do not claim progress or completion that is not present in the activity.
979
1060
  Use the prior TLDRs for context and the new activity for the update.
980
1061
  Summarize the current state of work; do not narrate the history.
981
1062
  If context is sparse, still summarize the available activity.
@@ -986,9 +1067,44 @@ Prefer verb + direct object. Include outcome only if important.
986
1067
  Do not address the user.
987
1068
  Output only the status fragment itself. No prefixes, labels, bullets, or quotes.
988
1069
  Plain text only; no markdown, JSON, code, file paths, or tool names.
1070
+
1071
+ Good examples:
1072
+ ${goodExamples}
1073
+
1074
+ Bad examples:
1075
+ - Editing extensions/status-footer.ts with success.
1076
+ - Reading status-footer file completed successfully.
1077
+ - Publishing \`pi-bar@0.3.3\` to npm.
1078
+ - Grepping for sanitizeTldrText callers.
1079
+ - Continuing with task progression.
1080
+ - Investigating user input responses.
1081
+
989
1082
  ${tenseInstruction}`;
990
1083
  }
991
1084
 
1085
+ function extractStopReason(message: unknown): string | undefined {
1086
+ if (!message || typeof message !== "object") return undefined;
1087
+ const record = message as Record<string, unknown>;
1088
+ const stopReason = record.stopReason;
1089
+ return typeof stopReason === "string" ? stopReason : undefined;
1090
+ }
1091
+
1092
+ // Two TLDRs are near-duplicates if their normalized action fragments (lowercase,
1093
+ // punctuation/whitespace collapsed) match. Catches bursts of cosmetic variants
1094
+ // like "Editing X." vs "Editing X" vs "editing x.".
1095
+ function normalizedActionFragment(text: string): string {
1096
+ return text
1097
+ .toLowerCase()
1098
+ .replace(/[\p{P}\p{S}]+/gu, " ")
1099
+ .replace(/\s+/gu, " ")
1100
+ .trim();
1101
+ }
1102
+
1103
+ function isNearDuplicateTldr(current: string, previous: string): boolean {
1104
+ if (!previous) return false;
1105
+ return normalizedActionFragment(current) === normalizedActionFragment(previous);
1106
+ }
1107
+
992
1108
  function previousCheckpointLines(checkpoints: readonly TldrCheckpoint[]): string {
993
1109
  if (checkpoints.length === 0) return "none";
994
1110
  return checkpoints
@@ -1110,6 +1226,7 @@ function stripTerminalControls(text: string): string {
1110
1226
  const LEAKED_PREFIX_PATTERN =
1111
1227
  /^\s*(?:[-*•]\s*)?(?:(?:through\s+activity|activity|checkpoint)\s+\d+\s*[:.\-—–]\s*|(?:tldr|summary)\s*[:.\-—–]\s*)+/i;
1112
1228
  const LEADING_PUNCT_PATTERN = /^[\s\-—–•*:#.,;]+/;
1229
+ const TRAILING_PUNCT_PATTERN = /[\s\-—–•*:#.,;]+$/;
1113
1230
 
1114
1231
  function stripLeakedScaffolding(text: string): string {
1115
1232
  let cleaned = text;
@@ -1122,13 +1239,84 @@ function stripLeakedScaffolding(text: string): string {
1122
1239
  return cleaned;
1123
1240
  }
1124
1241
 
1242
+ // Strip backticks (` and triple-backtick fences) and markdown emphasis markers
1243
+ // the model occasionally adds despite the prompt's plain-text rule. We keep
1244
+ // inner content verbatim; only the formatting characters disappear.
1245
+ function stripMarkdownFormatting(text: string): string {
1246
+ return text
1247
+ .replace(/```+/g, "")
1248
+ .replace(/`+/g, "")
1249
+ .replace(/(^|[^\\])([*_~]{1,3})(.+?)\2/g, "$1$3");
1250
+ }
1251
+
1252
+ // File-path / version / package leaks observed in backtests:
1253
+ // extensions/status-footer.ts, README.md, package.json, pi-bar@0.3.3, …
1254
+ const FILE_PATH_PATTERN =
1255
+ /(?:\b[\w./@-]+\.(?:ts|tsx|js|jsx|mjs|cjs|md|json|yml|yaml|toml|lock|sh|py|rs|go|html|css))\b/g;
1256
+ const PACKAGE_VERSION_PATTERN = /\b[\w./@-]+@\d[\w.+-]*\b/g;
1257
+ const VERSION_PATTERN = /\bv?\d+\.\d+(?:\.\d+(?:[-+][\w.]+)?)?\b/g;
1258
+
1259
+ function stripIdentifierLeaks(text: string): string {
1260
+ return text
1261
+ .replace(PACKAGE_VERSION_PATTERN, "")
1262
+ .replace(FILE_PATH_PATTERN, "")
1263
+ .replace(VERSION_PATTERN, "")
1264
+ .replace(/\s+/g, " ")
1265
+ .trim();
1266
+ }
1267
+
1268
+ // After identifier removal the sentence often has a dangling preposition like
1269
+ // "Bumping version to for publishing" (was "to 0.3.2 for publishing") or a
1270
+ // trailing "to"/"version". Run in a fix-point loop so chained leftovers (e.g.
1271
+ // "to version") collapse cleanly.
1272
+ const DANGLING_TRAILING_PREP_PATTERN =
1273
+ /\s+(?:to|at|as|of|by|for|in|on|with|from|version|v)\s*[.!?,;:]?\s*$/i;
1274
+ const DANGLING_PREP_CHAIN_PATTERN =
1275
+ /\b(to|at|as|of|by|from|version|v)\s+(for|in|on|with|from|after|before|during|to|at|as|of|by|and|but|or)\b/gi;
1276
+
1277
+ function stripDanglingPrepositions(text: string): string {
1278
+ let cleaned = text;
1279
+ let previous: string;
1280
+ do {
1281
+ previous = cleaned;
1282
+ cleaned = cleaned.replace(DANGLING_PREP_CHAIN_PATTERN, "$2");
1283
+ cleaned = cleaned.replace(DANGLING_TRAILING_PREP_PATTERN, "").trim();
1284
+ } while (cleaned !== previous && cleaned.length > 0);
1285
+ return cleaned;
1286
+ }
1287
+
1288
+ // Trailing filler suffixes observed in backtests:
1289
+ // "... with success.", "... completed successfully.", "... successfully."
1290
+ const SUCCESS_SUFFIX_PATTERN =
1291
+ /[\s,;:—–-]*(?:with\s+success|completed\s+successfully|finished\s+successfully|done\s+successfully|successfully\s+completed|successfully\s+finished|successfully)\s*[.!?]*\s*$/i;
1292
+
1293
+ function stripSuccessSuffix(text: string): string {
1294
+ let cleaned = text;
1295
+ let stripped = false;
1296
+ while (true) {
1297
+ const next = cleaned.replace(SUCCESS_SUFFIX_PATTERN, "").trim();
1298
+ if (next === cleaned || next.length === 0) break;
1299
+ cleaned = next;
1300
+ stripped = true;
1301
+ }
1302
+ // Only clean up trailing punctuation when an actual success suffix was
1303
+ // removed; otherwise we would strip the natural period from a normal
1304
+ // sentence like "Reviewing footer behavior.".
1305
+ if (stripped) cleaned = cleaned.replace(TRAILING_PUNCT_PATTERN, "").trim();
1306
+ return cleaned;
1307
+ }
1308
+
1125
1309
  function sanitizeTldrText(
1126
1310
  text: string,
1127
1311
  maxChars = MAX_SAFE_TLDR_CHARS,
1128
1312
  ): string {
1129
1313
  const stripped = stripTerminalControls(text);
1130
- const cleaned = stripLeakedScaffolding(stripped) || stripped;
1131
- return truncateText(cleaned, maxChars);
1314
+ const withoutMarkdown = stripMarkdownFormatting(stripped);
1315
+ const withoutScaffolding = stripLeakedScaffolding(withoutMarkdown) || withoutMarkdown;
1316
+ const withoutLeaks = stripIdentifierLeaks(withoutScaffolding) || withoutScaffolding;
1317
+ const withoutDangling = stripDanglingPrepositions(withoutLeaks) || withoutLeaks;
1318
+ const withoutSuccess = stripSuccessSuffix(withoutDangling) || withoutDangling;
1319
+ return truncateText(withoutSuccess, maxChars);
1132
1320
  }
1133
1321
 
1134
1322
  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.26",
3
+ "version": "0.3.28",
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.26/assets/screenshot-yellow.png"
54
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.28/assets/screenshot-yellow.png"
55
55
  }
56
56
  }