pi-bar 0.3.25 → 0.3.27

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.25/assets/screenshot-green.png)
13
- ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.25/assets/screenshot-yellow.png)
14
- ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.25/assets/screenshot-red.png)
12
+ ![pi-bar with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.27/assets/screenshot-green.png)
13
+ ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.27/assets/screenshot-yellow.png)
14
+ ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.27/assets/screenshot-red.png)
15
15
 
16
16
  ## Why use it?
17
17
 
@@ -43,11 +43,7 @@ type SerializedStatusFilter =
43
43
  type TldrActivityType =
44
44
  | "user_message"
45
45
  | "assistant_update"
46
- | "tool_input_start"
47
- | "tool_input_update"
48
- | "tool_input_end"
49
46
  | "tool_call"
50
- | "tool_execution_update"
51
47
  | "tool_result"
52
48
  | "assistant_final"
53
49
  | "assistant_failure";
@@ -58,36 +54,17 @@ type TldrActivity = {
58
54
  displayPriority: TldrDisplayPriority;
59
55
  text: string;
60
56
  toolCallId?: string;
61
- // Identifies a continuous streaming run (tool input arg streaming, or tool
62
- // execution output streaming). Activities sharing a group represent updates
63
- // to the same task; staleness handling treats them as one stream.
64
- progressGroup?: string;
65
57
  };
66
58
  type TldrCheckpoint = {
67
59
  activityIndex: number;
68
- activityType: TldrActivityType;
69
60
  displayPriority: TldrDisplayPriority;
70
61
  text: string;
71
- progressGroup?: string;
72
62
  };
73
63
  type TldrCheckpointJob = {
74
64
  activityIndex: number;
75
- activityType: TldrActivityType;
76
65
  displayPriority: TldrDisplayPriority;
77
66
  runId: number;
78
- progressGroup?: string;
79
67
  };
80
-
81
- // Activities that represent ongoing streaming work. These are treated
82
- // specially: their checkpoints can render even when newer same-stream activity
83
- // exists, and the display refresh slows to a 10s cadence for repeat renders so
84
- // the footer does not become a model-call ticker.
85
- function isToolProgressActivity(activityType: TldrActivityType): boolean {
86
- return (
87
- activityType === "tool_input_update" ||
88
- activityType === "tool_execution_update"
89
- );
90
- }
91
68
  type TldrModelPreference = { provider: string; id: string };
92
69
  type FastModelAuth = {
93
70
  model: Parameters<typeof complete>[0];
@@ -111,16 +88,12 @@ const TLDR_MAX_TOKENS = 120;
111
88
  const TLDR_REQUEST_TIMEOUT_MS = 2_000;
112
89
  const TLDR_DISPLAY_UPDATE_INTERVAL_MS = 1_200;
113
90
  const TLDR_TARGET_SUMMARY_CHARS = 60;
114
- // While rendering successive progress checkpoints from the same stream (e.g.
115
- // long bash output), slow the display refresh dramatically. The TLDR text
116
- // barely changes between adjacent chunks; refreshing every 1.2s would burn API
117
- // calls and footer renders for noise.
118
- const TOOL_PROGRESS_DISPLAY_UPDATE_INTERVAL_MS = 10_000;
119
- const MAX_TOOL_PROGRESS_PARTIAL_CHARS = 280;
120
91
  // Burst debouncing on the generation side: coalesce rapid-fire activities into
121
92
  // one model call. Quiet window catches short bursts; max wait surfaces progress
122
93
  // during continuous activity.
123
- 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;
124
97
  const NORMAL_CHECKPOINT_MAX_WAIT_MS = 2_500;
125
98
  // Defensive ceiling: even if the model ignores the length instruction, never
126
99
  // blast a large payload into the footer.
@@ -477,58 +450,13 @@ function extractTextContent(content: readonly unknown[] | undefined): string | u
477
450
  return compacted.length > 0 ? compacted : undefined;
478
451
  }
479
452
 
480
- type ToolInputAssistantEvent =
481
- | { type: "toolcall_start"; contentIndex: number; partial: unknown }
482
- | {
483
- type: "toolcall_delta";
484
- contentIndex: number;
485
- delta: string;
486
- partial: unknown;
487
- }
488
- | {
489
- type: "toolcall_end";
490
- contentIndex: number;
491
- toolCall: { id: string; name: string; arguments?: unknown };
492
- partial: unknown;
493
- };
494
-
495
- function toolCallAtContentIndex(
496
- partial: unknown,
497
- contentIndex: number,
498
- ): { id: string; name: string; arguments?: unknown } | undefined {
499
- if (!partial || typeof partial !== "object") return undefined;
500
- const content = (partial as { content?: unknown }).content;
501
- if (!Array.isArray(content)) return undefined;
502
- const block = content[contentIndex];
503
- if (!block || typeof block !== "object") return undefined;
504
- const record = block as Record<string, unknown>;
505
- if (record.type !== "toolCall") return undefined;
506
- if (typeof record.id !== "string" || typeof record.name !== "string") {
507
- return undefined;
508
- }
509
- return {
510
- id: record.id,
511
- name: record.name,
512
- arguments: record.arguments,
513
- };
514
- }
515
-
516
453
  class TldrFactCollector {
517
454
  private nextIndex = 1;
518
455
  private readonly activities: TldrActivity[] = [];
519
- // Streaming tool inputs (`toolcall_start/delta/end` from message_update)
520
- // arrive before the final tool id is available; content index is the best
521
- // stable anchor until the id appears.
522
- private readonly activeToolInputAnchors = new Map<string, string>();
523
456
 
524
457
  resetConversation(): void {
525
458
  this.nextIndex = 1;
526
459
  this.activities.splice(0);
527
- this.activeToolInputAnchors.clear();
528
- }
529
-
530
- latestActivity(): TldrActivity | undefined {
531
- return this.activities[this.activities.length - 1];
532
460
  }
533
461
 
534
462
  recordUserMessage(prompt: string): TldrActivity {
@@ -560,79 +488,6 @@ class TldrFactCollector {
560
488
  );
561
489
  }
562
490
 
563
- // Stage 1: tool input streaming. The model is writing tool name + args; pi
564
- // surfaces this via `assistantMessageEvent` on `message_update` with
565
- // toolcall_start / toolcall_delta / toolcall_end. Useful when args are long
566
- // (big edits, structured plans) and the user wants visible progress before
567
- // the actual tool execution starts.
568
- recordToolInputActivity(
569
- event: ToolInputAssistantEvent | undefined,
570
- ): TldrActivity | undefined {
571
- if (!event) return undefined;
572
-
573
- switch (event.type) {
574
- case "toolcall_start": {
575
- const toolCall = toolCallAtContentIndex(event.partial, event.contentIndex);
576
- const toolName = toolCall?.name ?? "unknown";
577
- const anchorKey = toolCall?.id ?? `content:${event.contentIndex}`;
578
- const anchorText = `tool input: ${toolName}`;
579
- this.activeToolInputAnchors.set(anchorKey, anchorText);
580
- return this.addActivity(
581
- "tool_input_start",
582
- "normal",
583
- anchorText,
584
- toolCall?.id,
585
- );
586
- }
587
- case "toolcall_delta": {
588
- const toolCall = toolCallAtContentIndex(event.partial, event.contentIndex);
589
- const anchorKey = toolCall?.id ?? `content:${event.contentIndex}`;
590
- const anchorText =
591
- this.activeToolInputAnchors.get(anchorKey) ??
592
- `tool input: ${toolCall?.name ?? "unknown"}`;
593
-
594
- // Repeated delta updates for the same call collapse into the latest
595
- // snapshot. Otherwise long arg streams would push out useful facts.
596
- const last = this.activities[this.activities.length - 1];
597
- if (
598
- last?.activityType === "tool_input_update" &&
599
- last.progressGroup === `tool-input:${anchorKey}`
600
- ) {
601
- this.activities.pop();
602
- }
603
-
604
- const argSnapshot = compactValue(toolCall?.arguments, 200);
605
- const chunk = event.delta
606
- ? `latest: ${truncateText(compactText(event.delta), 120)}`
607
- : undefined;
608
- const update = [argSnapshot, chunk].filter(Boolean).join("; ");
609
-
610
- return this.addActivity(
611
- "tool_input_update",
612
- "normal",
613
- `${anchorText}; ${update || "streaming"}`,
614
- toolCall?.id,
615
- `tool-input:${anchorKey}`,
616
- );
617
- }
618
- case "toolcall_end": {
619
- const anchorKey = event.toolCall.id;
620
- const anchorText =
621
- this.activeToolInputAnchors.get(anchorKey) ??
622
- `tool input: ${event.toolCall.name}`;
623
- this.activeToolInputAnchors.delete(anchorKey);
624
-
625
- const args = compactValue(event.toolCall.arguments, 240);
626
- return this.addActivity(
627
- "tool_input_end",
628
- "normal",
629
- args ? `${anchorText} completed; ${args}` : `${anchorText} completed`,
630
- event.toolCall.id,
631
- );
632
- }
633
- }
634
- }
635
-
636
491
  recordToolCall(event: {
637
492
  toolName: string;
638
493
  input?: Record<string, unknown>;
@@ -646,45 +501,6 @@ class TldrFactCollector {
646
501
  );
647
502
  }
648
503
 
649
- // Stage 2: tool execution streaming. The host is running the tool and
650
- // emitting partial output. Tagged with progressGroup so adjacent chunks of
651
- // the same execution are treated as one continuous stream.
652
- recordToolExecutionUpdate(event: {
653
- toolCallId: string;
654
- toolName: string;
655
- partialResult: unknown;
656
- }): TldrActivity {
657
- const partialText =
658
- extractTextContent(
659
- Array.isArray((event.partialResult as { content?: unknown })?.content)
660
- ? ((event.partialResult as { content: unknown[] }).content as unknown[])
661
- : undefined,
662
- ) ??
663
- compactValue(event.partialResult, MAX_TOOL_PROGRESS_PARTIAL_CHARS) ??
664
- "streaming";
665
-
666
- const progressGroup = `tool-execution:${event.toolCallId}`;
667
-
668
- // Adjacent execution-update activities for the same stream collapse into
669
- // the latest. The retained activity list stays bounded and the model sees
670
- // one current snapshot of in-progress output.
671
- const last = this.activities[this.activities.length - 1];
672
- if (
673
- last?.activityType === "tool_execution_update" &&
674
- last.progressGroup === progressGroup
675
- ) {
676
- this.activities.pop();
677
- }
678
-
679
- return this.addActivity(
680
- "tool_execution_update",
681
- "normal",
682
- `running ${event.toolName}; ${tailText(partialText, MAX_TOOL_PROGRESS_PARTIAL_CHARS)}`,
683
- event.toolCallId,
684
- progressGroup,
685
- );
686
- }
687
-
688
504
  recordToolResult(event: {
689
505
  toolName: string;
690
506
  input?: Record<string, unknown>;
@@ -703,24 +519,15 @@ class TldrFactCollector {
703
519
  const status = isError ? "error" : "ok";
704
520
  const tool = compactToolInput(event.toolName, event.input);
705
521
 
706
- // The completed result fact subsumes all in-progress facts for the same
707
- // call: the original tool_call, any tool_input_* lifecycle facts, and any
708
- // tool_execution_update progress. The model only needs the final outcome.
522
+ // Result fact subsumes the prior tool_call fact for the same call. Drop the
523
+ // matching tool_call so the model sees one combined fact instead of two.
709
524
  if (event.toolCallId) {
710
- const id = event.toolCallId;
711
- for (let i = this.activities.length - 1; i >= 0; i--) {
712
- const activity = this.activities[i];
713
- if (activity.toolCallId !== id) continue;
714
- if (
715
- activity.activityType === "tool_call" ||
716
- activity.activityType === "tool_input_start" ||
717
- activity.activityType === "tool_input_update" ||
718
- activity.activityType === "tool_input_end" ||
719
- activity.activityType === "tool_execution_update"
720
- ) {
721
- this.activities.splice(i, 1);
722
- }
723
- }
525
+ const idx = this.activities.findIndex(
526
+ (activity) =>
527
+ activity.activityType === "tool_call" &&
528
+ activity.toolCallId === event.toolCallId,
529
+ );
530
+ if (idx !== -1) this.activities.splice(idx, 1);
724
531
  }
725
532
 
726
533
  return this.addActivity(
@@ -788,15 +595,13 @@ class TldrFactCollector {
788
595
  displayPriority: TldrDisplayPriority,
789
596
  text: string,
790
597
  toolCallId?: string,
791
- progressGroup?: string,
792
598
  ): TldrActivity {
793
- const activity: TldrActivity = {
599
+ const activity = {
794
600
  index: this.nextIndex,
795
601
  activityType,
796
602
  displayPriority,
797
603
  text: truncateText(text, MAX_ACTIVITY_TEXT_CHARS),
798
604
  toolCallId,
799
- progressGroup,
800
605
  };
801
606
  this.nextIndex++;
802
607
  this.activities.push(activity);
@@ -828,9 +633,6 @@ class FooterTldrEngine {
828
633
  private pendingNormalCheckpoint?: TldrCheckpointJob;
829
634
  private normalCheckpointTimer?: ReturnType<typeof setTimeout>;
830
635
  private normalCheckpointBurstStartedAt?: number;
831
- // Last progress stream that produced a rendered checkpoint. Used to slow the
832
- // display refresh for repeat renders from the same stream.
833
- private lastRenderedProgressGroup?: string;
834
636
 
835
637
  constructor(private readonly requestRender: () => void) {}
836
638
 
@@ -850,47 +652,32 @@ class FooterTldrEngine {
850
652
  }
851
653
 
852
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.
853
659
  this.currentText = null;
660
+ this.lastRenderedText = "";
661
+ this.acceptedCheckpoints.splice(0);
662
+ this.latestAcceptedActivityIndex = 0;
854
663
  this.requestRender();
855
664
  this.enqueue(ctx, this.facts.recordUserMessage(prompt));
856
665
  }
857
666
 
858
- recordAssistantUpdate(
859
- ctx: ExtensionContext,
860
- message: unknown,
861
- assistantMessageEvent?: unknown,
862
- ): void {
863
- // Tool input streaming hides inside `message_update`. Drain that signal
864
- // first; only if no tool-input fact was produced does this look like a
865
- // plain assistant text update worth recording.
866
- const toolInputActivity = this.facts.recordToolInputActivity(
867
- normalizeToolInputEvent(assistantMessageEvent),
868
- );
869
- if (toolInputActivity) {
870
- this.enqueue(ctx, toolInputActivity);
871
- return;
872
- }
873
-
667
+ recordAssistantUpdate(ctx: ExtensionContext, message: unknown): void {
874
668
  const activity = this.facts.recordAssistantUpdate(message);
875
669
  if (activity) this.enqueue(ctx, activity);
876
670
  }
877
671
 
878
672
  recordToolCall(
879
- ctx: ExtensionContext,
673
+ _ctx: ExtensionContext,
880
674
  event: { toolName: string; input?: Record<string, unknown>; toolCallId?: string },
881
675
  ): void {
882
- this.enqueue(ctx, this.facts.recordToolCall(event));
883
- }
884
-
885
- recordToolExecutionUpdate(
886
- ctx: ExtensionContext,
887
- event: {
888
- toolCallId: string;
889
- toolName: string;
890
- partialResult: unknown;
891
- },
892
- ): void {
893
- this.enqueue(ctx, this.facts.recordToolExecutionUpdate(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);
894
681
  }
895
682
 
896
683
  recordToolResult(
@@ -915,9 +702,33 @@ class FooterTldrEngine {
915
702
  this.startFreshRun();
916
703
  return;
917
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
+
918
716
  this.enqueue(ctx, result);
919
717
  }
920
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
+
921
732
  private startFreshRun(): void {
922
733
  this.runId++;
923
734
  this.cancelWork();
@@ -927,7 +738,6 @@ class FooterTldrEngine {
927
738
  this.lastDisplayAt = Number.NEGATIVE_INFINITY;
928
739
  this.acceptedCheckpoints.splice(0);
929
740
  this.pendingDisplayCheckpoint = undefined;
930
- this.lastRenderedProgressGroup = undefined;
931
741
  this.currentText = null;
932
742
  this.requestRender();
933
743
  }
@@ -996,12 +806,10 @@ class FooterTldrEngine {
996
806
 
997
807
  private enqueue(ctx: ExtensionContext, activity: TldrActivity): void {
998
808
  if (!ctx.hasUI) return;
999
- const job: TldrCheckpointJob = {
809
+ const job = {
1000
810
  activityIndex: activity.index,
1001
- activityType: activity.activityType,
1002
811
  displayPriority: activity.displayPriority,
1003
812
  runId: this.runId,
1004
- progressGroup: activity.progressGroup,
1005
813
  };
1006
814
 
1007
815
  if (job.displayPriority === "immediate") {
@@ -1035,13 +843,9 @@ class FooterTldrEngine {
1035
843
  this.scheduleNormalCheckpoint(ctx, job);
1036
844
  }
1037
845
 
1038
- private clearDisplayTimer(): void {
846
+ private clearPendingDisplay(): void {
1039
847
  if (this.displayTimer) clearTimeout(this.displayTimer);
1040
848
  this.displayTimer = undefined;
1041
- }
1042
-
1043
- private clearPendingDisplay(): void {
1044
- this.clearDisplayTimer();
1045
849
  this.pendingDisplayCheckpoint = undefined;
1046
850
  }
1047
851
 
@@ -1099,12 +903,10 @@ class FooterTldrEngine {
1099
903
  const rawText = extractTextContent(response.content) ?? "";
1100
904
  const text = sanitizeTldrText(rawText);
1101
905
  if (!text) return;
1102
- const checkpoint: TldrCheckpoint = {
906
+ const checkpoint = {
1103
907
  activityIndex: job.activityIndex,
1104
- activityType: job.activityType,
1105
908
  displayPriority: job.displayPriority,
1106
909
  text,
1107
- progressGroup: job.progressGroup,
1108
910
  };
1109
911
  this.acceptCheckpoint(checkpoint);
1110
912
  this.considerDisplayingCheckpoint(ctx, checkpoint);
@@ -1136,7 +938,7 @@ class FooterTldrEngine {
1136
938
  checkpoint: TldrCheckpoint,
1137
939
  ): void {
1138
940
  if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
1139
- if (this.shouldDropStaleCheckpoint(checkpoint)) return;
941
+ if (checkpoint.activityIndex !== this.facts.latestActivityIndex()) return;
1140
942
 
1141
943
  if (checkpoint.displayPriority !== "normal" || !this.lastRenderedText) {
1142
944
  this.clearPendingDisplay();
@@ -1144,77 +946,37 @@ class FooterTldrEngine {
1144
946
  return;
1145
947
  }
1146
948
 
1147
- const displayIntervalMs = this.displayIntervalFor(checkpoint);
1148
949
  const elapsedMs = performance.now() - this.lastDisplayAt;
1149
- if (elapsedMs >= displayIntervalMs) {
950
+ if (elapsedMs >= TLDR_DISPLAY_UPDATE_INTERVAL_MS) {
1150
951
  this.clearPendingDisplay();
1151
952
  this.renderCheckpoint(checkpoint);
1152
953
  return;
1153
954
  }
1154
955
 
1155
956
  this.pendingDisplayCheckpoint = checkpoint;
1156
- // Recompute the timer for each newly accepted pending checkpoint. A long
1157
- // 10s progress-stream debounce should not block a subsequent ordinary
1158
- // update that wants to render at the normal 1.2s cadence.
1159
- this.clearDisplayTimer();
957
+ if (this.displayTimer) return;
1160
958
  this.displayTimer = setTimeout(() => {
1161
959
  this.displayTimer = undefined;
1162
960
  const pending = this.pendingDisplayCheckpoint;
1163
961
  this.pendingDisplayCheckpoint = undefined;
1164
- if (!pending) return;
1165
- if (this.shouldDropStaleCheckpoint(pending)) return;
962
+ if (!pending || pending.activityIndex !== this.facts.latestActivityIndex()) return;
1166
963
  this.renderCheckpoint(pending);
1167
- }, displayIntervalMs - elapsedMs);
1168
- }
1169
-
1170
- // A model call for chunk N may finish after chunk N+1 has been recorded.
1171
- // Most stale checkpoints are dropped, but progress streams are an exception:
1172
- // while the same stream is still active, a slightly-behind TLDR is still
1173
- // useful, and dropping it would leave the footer stuck on older text.
1174
- private shouldDropStaleCheckpoint(checkpoint: TldrCheckpoint): boolean {
1175
- if (checkpoint.activityIndex === this.facts.latestActivityIndex()) {
1176
- return false;
1177
- }
1178
-
1179
- // Boundary checkpoints (user_message, assistant_final, etc.) must stay
1180
- // latest-only. A stale prompt-start or final TLDR would describe the wrong
1181
- // turn entirely.
1182
- if (checkpoint.displayPriority !== "normal") return true;
1183
-
1184
- if (!isToolProgressActivity(checkpoint.activityType)) return true;
1185
- if (!checkpoint.progressGroup) return true;
1186
-
1187
- const latestActivity = this.facts.latestActivity();
1188
- return !(
1189
- latestActivity &&
1190
- latestActivity.index > checkpoint.activityIndex &&
1191
- latestActivity.activityType === checkpoint.activityType &&
1192
- latestActivity.progressGroup === checkpoint.progressGroup
1193
- );
1194
- }
1195
-
1196
- private displayIntervalFor(checkpoint: TldrCheckpoint): number {
1197
- if (!isToolProgressActivity(checkpoint.activityType)) {
1198
- return TLDR_DISPLAY_UPDATE_INTERVAL_MS;
1199
- }
1200
- // First render from a new progress group uses the normal cadence so the
1201
- // user sees the stream's first TLDR quickly. Subsequent renders for the
1202
- // same group slow down: the model output barely changes between chunks.
1203
- return checkpoint.progressGroup &&
1204
- checkpoint.progressGroup === this.lastRenderedProgressGroup
1205
- ? TOOL_PROGRESS_DISPLAY_UPDATE_INTERVAL_MS
1206
- : TLDR_DISPLAY_UPDATE_INTERVAL_MS;
964
+ }, TLDR_DISPLAY_UPDATE_INTERVAL_MS - elapsedMs);
1207
965
  }
1208
966
 
1209
967
  private renderCheckpoint(checkpoint: TldrCheckpoint): void {
1210
968
  if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
1211
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
+ }
1212
977
  this.lastRenderedActivityIndex = checkpoint.activityIndex;
1213
978
  this.lastRenderedText = checkpoint.text;
1214
979
  this.lastDisplayAt = performance.now();
1215
- this.lastRenderedProgressGroup = isToolProgressActivity(checkpoint.activityType)
1216
- ? checkpoint.progressGroup
1217
- : undefined;
1218
980
  this.currentText = checkpoint.text;
1219
981
  this.requestRender();
1220
982
  }
@@ -1252,12 +1014,19 @@ function checkpointSystemPrompt(job: TldrCheckpointJob): string {
1252
1014
  ? "Start with a past-tense verb."
1253
1015
  : "Start with a present-tense -ing verb.";
1254
1016
  // Frame the model as a human developer describing visible work, not an agent
1255
- // summarizing its own mechanics. Avoids leaks like tool names, prompt labels,
1256
- // or our internal scaffolding (activity indexes, prior checkpoints).
1017
+ // summarizing its own mechanics. The banned phrases and few-shot examples
1018
+ // fix concrete regressions observed in backtests: tool-name verbs, success
1019
+ // suffixes, file path leaks, and markdown formatting.
1257
1020
  return `Write one plain-English TLDR for a Pi coding agent.
1258
1021
  Describe the work progress as if a human developer were doing it.
1259
1022
  Focus on the task activity and current outcome, not agent mechanics.
1260
1023
  Do not mention tools, tool calls, prompts, messages, model output, or implementation details.
1024
+ Do not start with tool-name verbs such as Read, Reading, Grep, Listing, Counting, Extracting, Displaying, Editing, Writing, Running, Publishing.
1025
+ Use human-developer verbs instead: Reviewing, Investigating, Exploring, Updating, Refining, Fixing, Implementing, Wrapping up.
1026
+ Do not use file paths, file extensions, code identifiers, package names, or version strings.
1027
+ Do not use backticks, asterisks, underscores, quotes, or any markdown formatting.
1028
+ Do not append filler suffixes such as "with success", "successfully", or "completed successfully".
1029
+ Do not claim progress or completion that is not present in the activity.
1261
1030
  Use the prior TLDRs for context and the new activity for the update.
1262
1031
  Summarize the current state of work; do not narrate the history.
1263
1032
  If context is sparse, still summarize the available activity.
@@ -1268,9 +1037,45 @@ Prefer verb + direct object. Include outcome only if important.
1268
1037
  Do not address the user.
1269
1038
  Output only the status fragment itself. No prefixes, labels, bullets, or quotes.
1270
1039
  Plain text only; no markdown, JSON, code, file paths, or tool names.
1040
+
1041
+ Good examples:
1042
+ - Reviewing footer summary behavior
1043
+ - Investigating live TLDR regressions
1044
+ - Refining sanitizer for stray prefixes
1045
+ - Wrapping up extension release
1046
+
1047
+ Bad examples:
1048
+ - Editing extensions/status-footer.ts with success.
1049
+ - Reading status-footer file completed successfully.
1050
+ - Publishing \`pi-bar@0.3.3\` to npm.
1051
+ - Grepping for sanitizeTldrText callers.
1052
+
1271
1053
  ${tenseInstruction}`;
1272
1054
  }
1273
1055
 
1056
+ function extractStopReason(message: unknown): string | undefined {
1057
+ if (!message || typeof message !== "object") return undefined;
1058
+ const record = message as Record<string, unknown>;
1059
+ const stopReason = record.stopReason;
1060
+ return typeof stopReason === "string" ? stopReason : undefined;
1061
+ }
1062
+
1063
+ // Two TLDRs are near-duplicates if their normalized action fragments (lowercase,
1064
+ // punctuation/whitespace collapsed) match. Catches bursts of cosmetic variants
1065
+ // like "Editing X." vs "Editing X" vs "editing x.".
1066
+ function normalizedActionFragment(text: string): string {
1067
+ return text
1068
+ .toLowerCase()
1069
+ .replace(/[\p{P}\p{S}]+/gu, " ")
1070
+ .replace(/\s+/gu, " ")
1071
+ .trim();
1072
+ }
1073
+
1074
+ function isNearDuplicateTldr(current: string, previous: string): boolean {
1075
+ if (!previous) return false;
1076
+ return normalizedActionFragment(current) === normalizedActionFragment(previous);
1077
+ }
1078
+
1274
1079
  function previousCheckpointLines(checkpoints: readonly TldrCheckpoint[]): string {
1275
1080
  if (checkpoints.length === 0) return "none";
1276
1081
  return checkpoints
@@ -1283,50 +1088,6 @@ function formatRawActivity(activity: TldrActivity): string {
1283
1088
  return `- ${activity.text}`;
1284
1089
  }
1285
1090
 
1286
- // Narrows the unknown `assistantMessageEvent` from `message_update` to the
1287
- // three tool-input streaming variants. Other event shapes (text_start, etc.)
1288
- // produce undefined, signalling "no tool input fact to record".
1289
- function normalizeToolInputEvent(value: unknown): ToolInputAssistantEvent | undefined {
1290
- if (!value || typeof value !== "object") return undefined;
1291
- const record = value as Record<string, unknown>;
1292
- const type = record.type;
1293
- if (type !== "toolcall_start" && type !== "toolcall_delta" && type !== "toolcall_end") {
1294
- return undefined;
1295
- }
1296
- if (typeof record.contentIndex !== "number") return undefined;
1297
-
1298
- if (type === "toolcall_delta") {
1299
- return {
1300
- type,
1301
- contentIndex: record.contentIndex,
1302
- delta: typeof record.delta === "string" ? record.delta : "",
1303
- partial: record.partial,
1304
- };
1305
- }
1306
- if (type === "toolcall_end") {
1307
- const toolCall = record.toolCall;
1308
- if (
1309
- !toolCall ||
1310
- typeof toolCall !== "object" ||
1311
- typeof (toolCall as Record<string, unknown>).id !== "string" ||
1312
- typeof (toolCall as Record<string, unknown>).name !== "string"
1313
- ) {
1314
- return undefined;
1315
- }
1316
- return {
1317
- type,
1318
- contentIndex: record.contentIndex,
1319
- toolCall: toolCall as { id: string; name: string; arguments?: unknown },
1320
- partial: record.partial,
1321
- };
1322
- }
1323
- return {
1324
- type,
1325
- contentIndex: record.contentIndex,
1326
- partial: record.partial,
1327
- };
1328
- }
1329
-
1330
1091
  // Strips ANSI/CSI/OSC/DCS/SOS/PM/APC escape sequences from model output before
1331
1092
  // it lands in the terminal. The TLDR text is model-controlled and renders into
1332
1093
  // the footer; the system prompt asking for plain text is not a security
@@ -1436,6 +1197,7 @@ function stripTerminalControls(text: string): string {
1436
1197
  const LEAKED_PREFIX_PATTERN =
1437
1198
  /^\s*(?:[-*•]\s*)?(?:(?:through\s+activity|activity|checkpoint)\s+\d+\s*[:.\-—–]\s*|(?:tldr|summary)\s*[:.\-—–]\s*)+/i;
1438
1199
  const LEADING_PUNCT_PATTERN = /^[\s\-—–•*:#.,;]+/;
1200
+ const TRAILING_PUNCT_PATTERN = /[\s\-—–•*:#.,;]+$/;
1439
1201
 
1440
1202
  function stripLeakedScaffolding(text: string): string {
1441
1203
  let cleaned = text;
@@ -1448,13 +1210,63 @@ function stripLeakedScaffolding(text: string): string {
1448
1210
  return cleaned;
1449
1211
  }
1450
1212
 
1213
+ // Strip backticks (` and triple-backtick fences) and markdown emphasis markers
1214
+ // the model occasionally adds despite the prompt's plain-text rule. We keep
1215
+ // inner content verbatim; only the formatting characters disappear.
1216
+ function stripMarkdownFormatting(text: string): string {
1217
+ return text
1218
+ .replace(/```+/g, "")
1219
+ .replace(/`+/g, "")
1220
+ .replace(/(^|[^\\])([*_~]{1,3})(.+?)\2/g, "$1$3");
1221
+ }
1222
+
1223
+ // File-path / version / package leaks observed in backtests:
1224
+ // extensions/status-footer.ts, README.md, package.json, pi-bar@0.3.3, …
1225
+ const FILE_PATH_PATTERN =
1226
+ /(?:\b[\w./@-]+\.(?:ts|tsx|js|jsx|mjs|cjs|md|json|yml|yaml|toml|lock|sh|py|rs|go|html|css))\b/g;
1227
+ const PACKAGE_VERSION_PATTERN = /\b[\w./@-]+@\d[\w.+-]*\b/g;
1228
+ const VERSION_PATTERN = /\bv?\d+\.\d+(?:\.\d+(?:[-+][\w.]+)?)?\b/g;
1229
+
1230
+ function stripIdentifierLeaks(text: string): string {
1231
+ return text
1232
+ .replace(PACKAGE_VERSION_PATTERN, "")
1233
+ .replace(FILE_PATH_PATTERN, "")
1234
+ .replace(VERSION_PATTERN, "")
1235
+ .replace(/\s+/g, " ")
1236
+ .trim();
1237
+ }
1238
+
1239
+ // Trailing filler suffixes observed in backtests:
1240
+ // "... with success.", "... completed successfully.", "... successfully."
1241
+ const SUCCESS_SUFFIX_PATTERN =
1242
+ /[\s,;:—–-]*(?:with\s+success|completed\s+successfully|finished\s+successfully|done\s+successfully|successfully\s+completed|successfully\s+finished|successfully)\s*[.!?]*\s*$/i;
1243
+
1244
+ function stripSuccessSuffix(text: string): string {
1245
+ let cleaned = text;
1246
+ let stripped = false;
1247
+ while (true) {
1248
+ const next = cleaned.replace(SUCCESS_SUFFIX_PATTERN, "").trim();
1249
+ if (next === cleaned || next.length === 0) break;
1250
+ cleaned = next;
1251
+ stripped = true;
1252
+ }
1253
+ // Only clean up trailing punctuation when an actual success suffix was
1254
+ // removed; otherwise we would strip the natural period from a normal
1255
+ // sentence like "Reviewing footer behavior.".
1256
+ if (stripped) cleaned = cleaned.replace(TRAILING_PUNCT_PATTERN, "").trim();
1257
+ return cleaned;
1258
+ }
1259
+
1451
1260
  function sanitizeTldrText(
1452
1261
  text: string,
1453
1262
  maxChars = MAX_SAFE_TLDR_CHARS,
1454
1263
  ): string {
1455
1264
  const stripped = stripTerminalControls(text);
1456
- const cleaned = stripLeakedScaffolding(stripped) || stripped;
1457
- return truncateText(cleaned, maxChars);
1265
+ const withoutMarkdown = stripMarkdownFormatting(stripped);
1266
+ const withoutScaffolding = stripLeakedScaffolding(withoutMarkdown) || withoutMarkdown;
1267
+ const withoutLeaks = stripIdentifierLeaks(withoutScaffolding) || withoutScaffolding;
1268
+ const withoutSuccess = stripSuccessSuffix(withoutLeaks) || withoutLeaks;
1269
+ return truncateText(withoutSuccess, maxChars);
1458
1270
  }
1459
1271
 
1460
1272
  function shouldShowStatus(key: string, filter: StatusFilter): boolean {
@@ -1744,18 +1556,11 @@ export default function (pi: ExtensionAPI) {
1744
1556
  tldr.recordUserMessage(ctx, event.prompt);
1745
1557
  });
1746
1558
  pi.on("message_update", async (event, ctx) => {
1747
- tldr.recordAssistantUpdate(
1748
- ctx,
1749
- event.message,
1750
- (event as { assistantMessageEvent?: unknown }).assistantMessageEvent,
1751
- );
1559
+ tldr.recordAssistantUpdate(ctx, event.message);
1752
1560
  });
1753
1561
  pi.on("tool_call", async (event, ctx) => {
1754
1562
  tldr.recordToolCall(ctx, event);
1755
1563
  });
1756
- pi.on("tool_execution_update", async (event, ctx) => {
1757
- tldr.recordToolExecutionUpdate(ctx, event);
1758
- });
1759
1564
  pi.on("tool_result", async (event, ctx) => {
1760
1565
  tldr.recordToolResult(ctx, event);
1761
1566
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bar",
3
- "version": "0.3.25",
3
+ "version": "0.3.27",
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.25/assets/screenshot-yellow.png"
54
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.27/assets/screenshot-yellow.png"
55
55
  }
56
56
  }