pi-bar 0.3.24 → 0.3.25

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.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)
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)
15
15
 
16
16
  ## Why use it?
17
17
 
@@ -43,7 +43,11 @@ 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"
46
49
  | "tool_call"
50
+ | "tool_execution_update"
47
51
  | "tool_result"
48
52
  | "assistant_final"
49
53
  | "assistant_failure";
@@ -54,17 +58,36 @@ type TldrActivity = {
54
58
  displayPriority: TldrDisplayPriority;
55
59
  text: string;
56
60
  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;
57
65
  };
58
66
  type TldrCheckpoint = {
59
67
  activityIndex: number;
68
+ activityType: TldrActivityType;
60
69
  displayPriority: TldrDisplayPriority;
61
70
  text: string;
71
+ progressGroup?: string;
62
72
  };
63
73
  type TldrCheckpointJob = {
64
74
  activityIndex: number;
75
+ activityType: TldrActivityType;
65
76
  displayPriority: TldrDisplayPriority;
66
77
  runId: number;
78
+ progressGroup?: string;
67
79
  };
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
+ }
68
91
  type TldrModelPreference = { provider: string; id: string };
69
92
  type FastModelAuth = {
70
93
  model: Parameters<typeof complete>[0];
@@ -88,6 +111,12 @@ const TLDR_MAX_TOKENS = 120;
88
111
  const TLDR_REQUEST_TIMEOUT_MS = 2_000;
89
112
  const TLDR_DISPLAY_UPDATE_INTERVAL_MS = 1_200;
90
113
  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;
91
120
  // Burst debouncing on the generation side: coalesce rapid-fire activities into
92
121
  // one model call. Quiet window catches short bursts; max wait surfaces progress
93
122
  // during continuous activity.
@@ -448,13 +477,58 @@ function extractTextContent(content: readonly unknown[] | undefined): string | u
448
477
  return compacted.length > 0 ? compacted : undefined;
449
478
  }
450
479
 
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
+
451
516
  class TldrFactCollector {
452
517
  private nextIndex = 1;
453
518
  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>();
454
523
 
455
524
  resetConversation(): void {
456
525
  this.nextIndex = 1;
457
526
  this.activities.splice(0);
527
+ this.activeToolInputAnchors.clear();
528
+ }
529
+
530
+ latestActivity(): TldrActivity | undefined {
531
+ return this.activities[this.activities.length - 1];
458
532
  }
459
533
 
460
534
  recordUserMessage(prompt: string): TldrActivity {
@@ -486,6 +560,79 @@ class TldrFactCollector {
486
560
  );
487
561
  }
488
562
 
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
+
489
636
  recordToolCall(event: {
490
637
  toolName: string;
491
638
  input?: Record<string, unknown>;
@@ -499,6 +646,45 @@ class TldrFactCollector {
499
646
  );
500
647
  }
501
648
 
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
+
502
688
  recordToolResult(event: {
503
689
  toolName: string;
504
690
  input?: Record<string, unknown>;
@@ -517,15 +703,24 @@ class TldrFactCollector {
517
703
  const status = isError ? "error" : "ok";
518
704
  const tool = compactToolInput(event.toolName, event.input);
519
705
 
520
- // Result fact subsumes the prior tool_call fact for the same call. Drop the
521
- // matching tool_call so the model sees one combined fact instead of two.
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
709
  if (event.toolCallId) {
523
- const idx = this.activities.findIndex(
524
- (activity) =>
525
- activity.activityType === "tool_call" &&
526
- activity.toolCallId === event.toolCallId,
527
- );
528
- if (idx !== -1) this.activities.splice(idx, 1);
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
+ }
529
724
  }
530
725
 
531
726
  return this.addActivity(
@@ -593,13 +788,15 @@ class TldrFactCollector {
593
788
  displayPriority: TldrDisplayPriority,
594
789
  text: string,
595
790
  toolCallId?: string,
791
+ progressGroup?: string,
596
792
  ): TldrActivity {
597
- const activity = {
793
+ const activity: TldrActivity = {
598
794
  index: this.nextIndex,
599
795
  activityType,
600
796
  displayPriority,
601
797
  text: truncateText(text, MAX_ACTIVITY_TEXT_CHARS),
602
798
  toolCallId,
799
+ progressGroup,
603
800
  };
604
801
  this.nextIndex++;
605
802
  this.activities.push(activity);
@@ -631,6 +828,9 @@ class FooterTldrEngine {
631
828
  private pendingNormalCheckpoint?: TldrCheckpointJob;
632
829
  private normalCheckpointTimer?: ReturnType<typeof setTimeout>;
633
830
  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;
634
834
 
635
835
  constructor(private readonly requestRender: () => void) {}
636
836
 
@@ -655,7 +855,22 @@ class FooterTldrEngine {
655
855
  this.enqueue(ctx, this.facts.recordUserMessage(prompt));
656
856
  }
657
857
 
658
- recordAssistantUpdate(ctx: ExtensionContext, message: unknown): void {
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
+
659
874
  const activity = this.facts.recordAssistantUpdate(message);
660
875
  if (activity) this.enqueue(ctx, activity);
661
876
  }
@@ -667,6 +882,17 @@ class FooterTldrEngine {
667
882
  this.enqueue(ctx, this.facts.recordToolCall(event));
668
883
  }
669
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));
894
+ }
895
+
670
896
  recordToolResult(
671
897
  ctx: ExtensionContext,
672
898
  event: {
@@ -701,6 +927,7 @@ class FooterTldrEngine {
701
927
  this.lastDisplayAt = Number.NEGATIVE_INFINITY;
702
928
  this.acceptedCheckpoints.splice(0);
703
929
  this.pendingDisplayCheckpoint = undefined;
930
+ this.lastRenderedProgressGroup = undefined;
704
931
  this.currentText = null;
705
932
  this.requestRender();
706
933
  }
@@ -769,10 +996,12 @@ class FooterTldrEngine {
769
996
 
770
997
  private enqueue(ctx: ExtensionContext, activity: TldrActivity): void {
771
998
  if (!ctx.hasUI) return;
772
- const job = {
999
+ const job: TldrCheckpointJob = {
773
1000
  activityIndex: activity.index,
1001
+ activityType: activity.activityType,
774
1002
  displayPriority: activity.displayPriority,
775
1003
  runId: this.runId,
1004
+ progressGroup: activity.progressGroup,
776
1005
  };
777
1006
 
778
1007
  if (job.displayPriority === "immediate") {
@@ -806,9 +1035,13 @@ class FooterTldrEngine {
806
1035
  this.scheduleNormalCheckpoint(ctx, job);
807
1036
  }
808
1037
 
809
- private clearPendingDisplay(): void {
1038
+ private clearDisplayTimer(): void {
810
1039
  if (this.displayTimer) clearTimeout(this.displayTimer);
811
1040
  this.displayTimer = undefined;
1041
+ }
1042
+
1043
+ private clearPendingDisplay(): void {
1044
+ this.clearDisplayTimer();
812
1045
  this.pendingDisplayCheckpoint = undefined;
813
1046
  }
814
1047
 
@@ -866,10 +1099,12 @@ class FooterTldrEngine {
866
1099
  const rawText = extractTextContent(response.content) ?? "";
867
1100
  const text = sanitizeTldrText(rawText);
868
1101
  if (!text) return;
869
- const checkpoint = {
1102
+ const checkpoint: TldrCheckpoint = {
870
1103
  activityIndex: job.activityIndex,
1104
+ activityType: job.activityType,
871
1105
  displayPriority: job.displayPriority,
872
1106
  text,
1107
+ progressGroup: job.progressGroup,
873
1108
  };
874
1109
  this.acceptCheckpoint(checkpoint);
875
1110
  this.considerDisplayingCheckpoint(ctx, checkpoint);
@@ -901,7 +1136,7 @@ class FooterTldrEngine {
901
1136
  checkpoint: TldrCheckpoint,
902
1137
  ): void {
903
1138
  if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
904
- if (checkpoint.activityIndex !== this.facts.latestActivityIndex()) return;
1139
+ if (this.shouldDropStaleCheckpoint(checkpoint)) return;
905
1140
 
906
1141
  if (checkpoint.displayPriority !== "normal" || !this.lastRenderedText) {
907
1142
  this.clearPendingDisplay();
@@ -909,22 +1144,66 @@ class FooterTldrEngine {
909
1144
  return;
910
1145
  }
911
1146
 
1147
+ const displayIntervalMs = this.displayIntervalFor(checkpoint);
912
1148
  const elapsedMs = performance.now() - this.lastDisplayAt;
913
- if (elapsedMs >= TLDR_DISPLAY_UPDATE_INTERVAL_MS) {
1149
+ if (elapsedMs >= displayIntervalMs) {
914
1150
  this.clearPendingDisplay();
915
1151
  this.renderCheckpoint(checkpoint);
916
1152
  return;
917
1153
  }
918
1154
 
919
1155
  this.pendingDisplayCheckpoint = checkpoint;
920
- if (this.displayTimer) return;
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();
921
1160
  this.displayTimer = setTimeout(() => {
922
1161
  this.displayTimer = undefined;
923
1162
  const pending = this.pendingDisplayCheckpoint;
924
1163
  this.pendingDisplayCheckpoint = undefined;
925
- if (!pending || pending.activityIndex !== this.facts.latestActivityIndex()) return;
1164
+ if (!pending) return;
1165
+ if (this.shouldDropStaleCheckpoint(pending)) return;
926
1166
  this.renderCheckpoint(pending);
927
- }, TLDR_DISPLAY_UPDATE_INTERVAL_MS - elapsedMs);
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;
928
1207
  }
929
1208
 
930
1209
  private renderCheckpoint(checkpoint: TldrCheckpoint): void {
@@ -933,6 +1212,9 @@ class FooterTldrEngine {
933
1212
  this.lastRenderedActivityIndex = checkpoint.activityIndex;
934
1213
  this.lastRenderedText = checkpoint.text;
935
1214
  this.lastDisplayAt = performance.now();
1215
+ this.lastRenderedProgressGroup = isToolProgressActivity(checkpoint.activityType)
1216
+ ? checkpoint.progressGroup
1217
+ : undefined;
936
1218
  this.currentText = checkpoint.text;
937
1219
  this.requestRender();
938
1220
  }
@@ -1001,6 +1283,50 @@ function formatRawActivity(activity: TldrActivity): string {
1001
1283
  return `- ${activity.text}`;
1002
1284
  }
1003
1285
 
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
+
1004
1330
  // Strips ANSI/CSI/OSC/DCS/SOS/PM/APC escape sequences from model output before
1005
1331
  // it lands in the terminal. The TLDR text is model-controlled and renders into
1006
1332
  // the footer; the system prompt asking for plain text is not a security
@@ -1418,11 +1744,18 @@ export default function (pi: ExtensionAPI) {
1418
1744
  tldr.recordUserMessage(ctx, event.prompt);
1419
1745
  });
1420
1746
  pi.on("message_update", async (event, ctx) => {
1421
- tldr.recordAssistantUpdate(ctx, event.message);
1747
+ tldr.recordAssistantUpdate(
1748
+ ctx,
1749
+ event.message,
1750
+ (event as { assistantMessageEvent?: unknown }).assistantMessageEvent,
1751
+ );
1422
1752
  });
1423
1753
  pi.on("tool_call", async (event, ctx) => {
1424
1754
  tldr.recordToolCall(ctx, event);
1425
1755
  });
1756
+ pi.on("tool_execution_update", async (event, ctx) => {
1757
+ tldr.recordToolExecutionUpdate(ctx, event);
1758
+ });
1426
1759
  pi.on("tool_result", async (event, ctx) => {
1427
1760
  tldr.recordToolResult(ctx, event);
1428
1761
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bar",
3
- "version": "0.3.24",
3
+ "version": "0.3.25",
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.24/assets/screenshot-yellow.png"
54
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.25/assets/screenshot-yellow.png"
55
55
  }
56
56
  }