pi-bar 0.3.23 → 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 +3 -3
- package/extensions/status-footer.ts +574 -36
- package/package.json +2 -2
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
|
-

|
|
13
|
+

|
|
14
|
+

|
|
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,20 @@ 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;
|
|
120
|
+
// Burst debouncing on the generation side: coalesce rapid-fire activities into
|
|
121
|
+
// one model call. Quiet window catches short bursts; max wait surfaces progress
|
|
122
|
+
// during continuous activity.
|
|
123
|
+
const NORMAL_CHECKPOINT_QUIET_MS = 700;
|
|
124
|
+
const NORMAL_CHECKPOINT_MAX_WAIT_MS = 2_500;
|
|
125
|
+
// Defensive ceiling: even if the model ignores the length instruction, never
|
|
126
|
+
// blast a large payload into the footer.
|
|
127
|
+
const MAX_SAFE_TLDR_CHARS = 240;
|
|
91
128
|
const CONFIG_PATH =
|
|
92
129
|
process.env.PI_BAR_CONFIG ?? join(homedir(), ".pi", "agent", "pi-bar.json");
|
|
93
130
|
|
|
@@ -236,11 +273,13 @@ function resolveTldrModelPreference(cwd: string): TldrModelPreference | undefine
|
|
|
236
273
|
return globalModel ? parseTldrModelSpec(globalModel) : undefined;
|
|
237
274
|
}
|
|
238
275
|
|
|
276
|
+
// Fallback order optimizes for low latency + low cost first. The TLDR path does
|
|
277
|
+
// not need reasoning quality; codex/gpt-5.4-mini is fastest in practice.
|
|
239
278
|
const FAST_TLDR_MODELS: readonly TldrModelPreference[] = [
|
|
240
|
-
{ provider: "anthropic", id: "claude-haiku-4-5" },
|
|
241
|
-
{ provider: "anthropic", id: "claude-haiku-4-5-20251001" },
|
|
242
279
|
{ provider: "openai-codex", id: "gpt-5.4-mini" },
|
|
243
280
|
{ provider: "openai-codex", id: "gpt-5.3-codex-spark" },
|
|
281
|
+
{ provider: "anthropic", id: "claude-haiku-4-5" },
|
|
282
|
+
{ provider: "anthropic", id: "claude-haiku-4-5-20251001" },
|
|
244
283
|
];
|
|
245
284
|
|
|
246
285
|
function formatTldrModelKey(model: TldrModelPreference): string {
|
|
@@ -281,8 +320,19 @@ async function getFastTldrModelAuth(
|
|
|
281
320
|
}
|
|
282
321
|
|
|
283
322
|
function truncateText(text: string, maxChars: number): string {
|
|
284
|
-
if (
|
|
285
|
-
|
|
323
|
+
if (maxChars <= 0) return "";
|
|
324
|
+
const chars = Array.from(text);
|
|
325
|
+
if (chars.length <= maxChars) return text;
|
|
326
|
+
if (maxChars === 1) return "…";
|
|
327
|
+
|
|
328
|
+
// Head+tail preserves both "what started" and "how it ended". Most useful for
|
|
329
|
+
// long bash/tool outputs where the final lines carry the outcome.
|
|
330
|
+
const retainedChars = maxChars - 1;
|
|
331
|
+
const headLength = Math.ceil(retainedChars / 2);
|
|
332
|
+
const tailLength = Math.floor(retainedChars / 2);
|
|
333
|
+
const head = chars.slice(0, headLength).join("");
|
|
334
|
+
const tail = tailLength > 0 ? chars.slice(-tailLength).join("") : "";
|
|
335
|
+
return `${head}…${tail}`;
|
|
286
336
|
}
|
|
287
337
|
|
|
288
338
|
function tailText(text: string, maxChars: number): string {
|
|
@@ -427,13 +477,58 @@ function extractTextContent(content: readonly unknown[] | undefined): string | u
|
|
|
427
477
|
return compacted.length > 0 ? compacted : undefined;
|
|
428
478
|
}
|
|
429
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
|
+
|
|
430
516
|
class TldrFactCollector {
|
|
431
517
|
private nextIndex = 1;
|
|
432
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>();
|
|
433
523
|
|
|
434
524
|
resetConversation(): void {
|
|
435
525
|
this.nextIndex = 1;
|
|
436
526
|
this.activities.splice(0);
|
|
527
|
+
this.activeToolInputAnchors.clear();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
latestActivity(): TldrActivity | undefined {
|
|
531
|
+
return this.activities[this.activities.length - 1];
|
|
437
532
|
}
|
|
438
533
|
|
|
439
534
|
recordUserMessage(prompt: string): TldrActivity {
|
|
@@ -465,6 +560,79 @@ class TldrFactCollector {
|
|
|
465
560
|
);
|
|
466
561
|
}
|
|
467
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
|
+
|
|
468
636
|
recordToolCall(event: {
|
|
469
637
|
toolName: string;
|
|
470
638
|
input?: Record<string, unknown>;
|
|
@@ -478,6 +646,45 @@ class TldrFactCollector {
|
|
|
478
646
|
);
|
|
479
647
|
}
|
|
480
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
|
+
|
|
481
688
|
recordToolResult(event: {
|
|
482
689
|
toolName: string;
|
|
483
690
|
input?: Record<string, unknown>;
|
|
@@ -496,15 +703,24 @@ class TldrFactCollector {
|
|
|
496
703
|
const status = isError ? "error" : "ok";
|
|
497
704
|
const tool = compactToolInput(event.toolName, event.input);
|
|
498
705
|
|
|
499
|
-
//
|
|
500
|
-
//
|
|
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.
|
|
501
709
|
if (event.toolCallId) {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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
|
+
}
|
|
508
724
|
}
|
|
509
725
|
|
|
510
726
|
return this.addActivity(
|
|
@@ -572,13 +788,15 @@ class TldrFactCollector {
|
|
|
572
788
|
displayPriority: TldrDisplayPriority,
|
|
573
789
|
text: string,
|
|
574
790
|
toolCallId?: string,
|
|
791
|
+
progressGroup?: string,
|
|
575
792
|
): TldrActivity {
|
|
576
|
-
const activity = {
|
|
793
|
+
const activity: TldrActivity = {
|
|
577
794
|
index: this.nextIndex,
|
|
578
795
|
activityType,
|
|
579
796
|
displayPriority,
|
|
580
797
|
text: truncateText(text, MAX_ACTIVITY_TEXT_CHARS),
|
|
581
798
|
toolCallId,
|
|
799
|
+
progressGroup,
|
|
582
800
|
};
|
|
583
801
|
this.nextIndex++;
|
|
584
802
|
this.activities.push(activity);
|
|
@@ -604,6 +822,15 @@ class FooterTldrEngine {
|
|
|
604
822
|
private displayTimer?: ReturnType<typeof setTimeout>;
|
|
605
823
|
private abortController?: AbortController;
|
|
606
824
|
private currentText: string | null = null;
|
|
825
|
+
// Normal-priority bursts wait for a quiet window before triggering a model
|
|
826
|
+
// call. Pi often emits many rapid activities (e.g. streaming reads); without
|
|
827
|
+
// debouncing we would hammer the TLDR model for intermediate states.
|
|
828
|
+
private pendingNormalCheckpoint?: TldrCheckpointJob;
|
|
829
|
+
private normalCheckpointTimer?: ReturnType<typeof setTimeout>;
|
|
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;
|
|
607
834
|
|
|
608
835
|
constructor(private readonly requestRender: () => void) {}
|
|
609
836
|
|
|
@@ -628,7 +855,22 @@ class FooterTldrEngine {
|
|
|
628
855
|
this.enqueue(ctx, this.facts.recordUserMessage(prompt));
|
|
629
856
|
}
|
|
630
857
|
|
|
631
|
-
recordAssistantUpdate(
|
|
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
|
+
|
|
632
874
|
const activity = this.facts.recordAssistantUpdate(message);
|
|
633
875
|
if (activity) this.enqueue(ctx, activity);
|
|
634
876
|
}
|
|
@@ -640,6 +882,17 @@ class FooterTldrEngine {
|
|
|
640
882
|
this.enqueue(ctx, this.facts.recordToolCall(event));
|
|
641
883
|
}
|
|
642
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
|
+
|
|
643
896
|
recordToolResult(
|
|
644
897
|
ctx: ExtensionContext,
|
|
645
898
|
event: {
|
|
@@ -674,6 +927,7 @@ class FooterTldrEngine {
|
|
|
674
927
|
this.lastDisplayAt = Number.NEGATIVE_INFINITY;
|
|
675
928
|
this.acceptedCheckpoints.splice(0);
|
|
676
929
|
this.pendingDisplayCheckpoint = undefined;
|
|
930
|
+
this.lastRenderedProgressGroup = undefined;
|
|
677
931
|
this.currentText = null;
|
|
678
932
|
this.requestRender();
|
|
679
933
|
}
|
|
@@ -681,28 +935,89 @@ class FooterTldrEngine {
|
|
|
681
935
|
private cancelWork(): void {
|
|
682
936
|
if (this.displayTimer) clearTimeout(this.displayTimer);
|
|
683
937
|
this.displayTimer = undefined;
|
|
938
|
+
this.clearPendingNormalCheckpoint();
|
|
684
939
|
this.checkpointQueue.splice(0);
|
|
685
940
|
this.inFlightCheckpoint = undefined;
|
|
686
941
|
this.abortController?.abort();
|
|
687
942
|
this.abortController = undefined;
|
|
688
943
|
}
|
|
689
944
|
|
|
945
|
+
private clearNormalCheckpointTimer(): void {
|
|
946
|
+
if (!this.normalCheckpointTimer) return;
|
|
947
|
+
clearTimeout(this.normalCheckpointTimer);
|
|
948
|
+
this.normalCheckpointTimer = undefined;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private clearPendingNormalCheckpoint(): void {
|
|
952
|
+
this.clearNormalCheckpointTimer();
|
|
953
|
+
this.pendingNormalCheckpoint = undefined;
|
|
954
|
+
this.normalCheckpointBurstStartedAt = undefined;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
private scheduleNormalCheckpoint(
|
|
958
|
+
ctx: ExtensionContext,
|
|
959
|
+
job: TldrCheckpointJob,
|
|
960
|
+
): void {
|
|
961
|
+
this.pendingNormalCheckpoint = job;
|
|
962
|
+
this.normalCheckpointBurstStartedAt ??= performance.now();
|
|
963
|
+
this.clearNormalCheckpointTimer();
|
|
964
|
+
|
|
965
|
+
const elapsedSinceBurstStarted =
|
|
966
|
+
performance.now() - this.normalCheckpointBurstStartedAt;
|
|
967
|
+
const maxWaitRemainingMs =
|
|
968
|
+
NORMAL_CHECKPOINT_MAX_WAIT_MS - elapsedSinceBurstStarted;
|
|
969
|
+
const delayMs = Math.max(
|
|
970
|
+
0,
|
|
971
|
+
Math.min(NORMAL_CHECKPOINT_QUIET_MS, maxWaitRemainingMs),
|
|
972
|
+
);
|
|
973
|
+
|
|
974
|
+
if (delayMs === 0) {
|
|
975
|
+
this.flushPendingNormalCheckpoint(ctx);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
this.normalCheckpointTimer = setTimeout(() => {
|
|
980
|
+
this.normalCheckpointTimer = undefined;
|
|
981
|
+
this.flushPendingNormalCheckpoint(ctx);
|
|
982
|
+
}, delayMs);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
private flushPendingNormalCheckpoint(ctx: ExtensionContext): void {
|
|
986
|
+
const job = this.pendingNormalCheckpoint;
|
|
987
|
+
this.clearPendingNormalCheckpoint();
|
|
988
|
+
if (!job || job.runId !== this.runId) return;
|
|
989
|
+
|
|
990
|
+
this.checkpointQueue = this.checkpointQueue.filter(
|
|
991
|
+
(queued) => queued.displayPriority !== "normal",
|
|
992
|
+
);
|
|
993
|
+
this.checkpointQueue.push(job);
|
|
994
|
+
this.pump(ctx);
|
|
995
|
+
}
|
|
996
|
+
|
|
690
997
|
private enqueue(ctx: ExtensionContext, activity: TldrActivity): void {
|
|
691
998
|
if (!ctx.hasUI) return;
|
|
692
|
-
const job = {
|
|
999
|
+
const job: TldrCheckpointJob = {
|
|
693
1000
|
activityIndex: activity.index,
|
|
1001
|
+
activityType: activity.activityType,
|
|
694
1002
|
displayPriority: activity.displayPriority,
|
|
695
1003
|
runId: this.runId,
|
|
1004
|
+
progressGroup: activity.progressGroup,
|
|
696
1005
|
};
|
|
697
1006
|
|
|
698
1007
|
if (job.displayPriority === "immediate") {
|
|
699
1008
|
this.clearPendingDisplay();
|
|
1009
|
+
this.clearPendingNormalCheckpoint();
|
|
700
1010
|
this.lastRenderedText = "";
|
|
701
1011
|
this.checkpointQueue.splice(0);
|
|
702
1012
|
this.abortInFlightCheckpoint();
|
|
703
1013
|
this.checkpointQueue.push(job);
|
|
704
|
-
|
|
1014
|
+
this.pump(ctx);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
if (job.displayPriority === "final") {
|
|
705
1019
|
this.clearPendingDisplay();
|
|
1020
|
+
this.clearPendingNormalCheckpoint();
|
|
706
1021
|
this.checkpointQueue = this.checkpointQueue.filter(
|
|
707
1022
|
(queued) => queued.displayPriority !== "normal",
|
|
708
1023
|
);
|
|
@@ -710,19 +1025,23 @@ class FooterTldrEngine {
|
|
|
710
1025
|
this.abortInFlightCheckpoint();
|
|
711
1026
|
}
|
|
712
1027
|
this.checkpointQueue.push(job);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
(queued) => queued.displayPriority !== "normal",
|
|
716
|
-
);
|
|
717
|
-
this.checkpointQueue.push(job);
|
|
1028
|
+
this.pump(ctx);
|
|
1029
|
+
return;
|
|
718
1030
|
}
|
|
719
1031
|
|
|
720
|
-
|
|
1032
|
+
// Normal-priority: debounce into a single coalesced job. The latest job
|
|
1033
|
+
// always wins because raw activity since the last accepted checkpoint is
|
|
1034
|
+
// what the model already sees, regardless of which job triggers the call.
|
|
1035
|
+
this.scheduleNormalCheckpoint(ctx, job);
|
|
721
1036
|
}
|
|
722
1037
|
|
|
723
|
-
private
|
|
1038
|
+
private clearDisplayTimer(): void {
|
|
724
1039
|
if (this.displayTimer) clearTimeout(this.displayTimer);
|
|
725
1040
|
this.displayTimer = undefined;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
private clearPendingDisplay(): void {
|
|
1044
|
+
this.clearDisplayTimer();
|
|
726
1045
|
this.pendingDisplayCheckpoint = undefined;
|
|
727
1046
|
}
|
|
728
1047
|
|
|
@@ -780,10 +1099,12 @@ class FooterTldrEngine {
|
|
|
780
1099
|
const rawText = extractTextContent(response.content) ?? "";
|
|
781
1100
|
const text = sanitizeTldrText(rawText);
|
|
782
1101
|
if (!text) return;
|
|
783
|
-
const checkpoint = {
|
|
1102
|
+
const checkpoint: TldrCheckpoint = {
|
|
784
1103
|
activityIndex: job.activityIndex,
|
|
1104
|
+
activityType: job.activityType,
|
|
785
1105
|
displayPriority: job.displayPriority,
|
|
786
1106
|
text,
|
|
1107
|
+
progressGroup: job.progressGroup,
|
|
787
1108
|
};
|
|
788
1109
|
this.acceptCheckpoint(checkpoint);
|
|
789
1110
|
this.considerDisplayingCheckpoint(ctx, checkpoint);
|
|
@@ -815,7 +1136,7 @@ class FooterTldrEngine {
|
|
|
815
1136
|
checkpoint: TldrCheckpoint,
|
|
816
1137
|
): void {
|
|
817
1138
|
if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
|
|
818
|
-
if (
|
|
1139
|
+
if (this.shouldDropStaleCheckpoint(checkpoint)) return;
|
|
819
1140
|
|
|
820
1141
|
if (checkpoint.displayPriority !== "normal" || !this.lastRenderedText) {
|
|
821
1142
|
this.clearPendingDisplay();
|
|
@@ -823,22 +1144,66 @@ class FooterTldrEngine {
|
|
|
823
1144
|
return;
|
|
824
1145
|
}
|
|
825
1146
|
|
|
1147
|
+
const displayIntervalMs = this.displayIntervalFor(checkpoint);
|
|
826
1148
|
const elapsedMs = performance.now() - this.lastDisplayAt;
|
|
827
|
-
if (elapsedMs >=
|
|
1149
|
+
if (elapsedMs >= displayIntervalMs) {
|
|
828
1150
|
this.clearPendingDisplay();
|
|
829
1151
|
this.renderCheckpoint(checkpoint);
|
|
830
1152
|
return;
|
|
831
1153
|
}
|
|
832
1154
|
|
|
833
1155
|
this.pendingDisplayCheckpoint = checkpoint;
|
|
834
|
-
|
|
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();
|
|
835
1160
|
this.displayTimer = setTimeout(() => {
|
|
836
1161
|
this.displayTimer = undefined;
|
|
837
1162
|
const pending = this.pendingDisplayCheckpoint;
|
|
838
1163
|
this.pendingDisplayCheckpoint = undefined;
|
|
839
|
-
if (!pending
|
|
1164
|
+
if (!pending) return;
|
|
1165
|
+
if (this.shouldDropStaleCheckpoint(pending)) return;
|
|
840
1166
|
this.renderCheckpoint(pending);
|
|
841
|
-
},
|
|
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;
|
|
842
1207
|
}
|
|
843
1208
|
|
|
844
1209
|
private renderCheckpoint(checkpoint: TldrCheckpoint): void {
|
|
@@ -847,6 +1212,9 @@ class FooterTldrEngine {
|
|
|
847
1212
|
this.lastRenderedActivityIndex = checkpoint.activityIndex;
|
|
848
1213
|
this.lastRenderedText = checkpoint.text;
|
|
849
1214
|
this.lastDisplayAt = performance.now();
|
|
1215
|
+
this.lastRenderedProgressGroup = isToolProgressActivity(checkpoint.activityType)
|
|
1216
|
+
? checkpoint.progressGroup
|
|
1217
|
+
: undefined;
|
|
850
1218
|
this.currentText = checkpoint.text;
|
|
851
1219
|
this.requestRender();
|
|
852
1220
|
}
|
|
@@ -883,7 +1251,13 @@ function checkpointSystemPrompt(job: TldrCheckpointJob): string {
|
|
|
883
1251
|
job.displayPriority === "final"
|
|
884
1252
|
? "Start with a past-tense verb."
|
|
885
1253
|
: "Start with a present-tense -ing verb.";
|
|
886
|
-
|
|
1254
|
+
// 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).
|
|
1257
|
+
return `Write one plain-English TLDR for a Pi coding agent.
|
|
1258
|
+
Describe the work progress as if a human developer were doing it.
|
|
1259
|
+
Focus on the task activity and current outcome, not agent mechanics.
|
|
1260
|
+
Do not mention tools, tool calls, prompts, messages, model output, or implementation details.
|
|
887
1261
|
Use the prior TLDRs for context and the new activity for the update.
|
|
888
1262
|
Summarize the current state of work; do not narrate the history.
|
|
889
1263
|
If context is sparse, still summarize the available activity.
|
|
@@ -892,7 +1266,6 @@ Return one concise status fragment under ${TLDR_TARGET_SUMMARY_CHARS} characters
|
|
|
892
1266
|
Omit subjects like "the agent" or "it".
|
|
893
1267
|
Prefer verb + direct object. Include outcome only if important.
|
|
894
1268
|
Do not address the user.
|
|
895
|
-
Do not mention activity numbers, indexes, checkpoints, prior TLDRs, or phrases like "through activity".
|
|
896
1269
|
Output only the status fragment itself. No prefixes, labels, bullets, or quotes.
|
|
897
1270
|
Plain text only; no markdown, JSON, code, file paths, or tool names.
|
|
898
1271
|
${tenseInstruction}`;
|
|
@@ -910,20 +1283,178 @@ function formatRawActivity(activity: TldrActivity): string {
|
|
|
910
1283
|
return `- ${activity.text}`;
|
|
911
1284
|
}
|
|
912
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
|
+
|
|
1330
|
+
// Strips ANSI/CSI/OSC/DCS/SOS/PM/APC escape sequences from model output before
|
|
1331
|
+
// it lands in the terminal. The TLDR text is model-controlled and renders into
|
|
1332
|
+
// the footer; the system prompt asking for plain text is not a security
|
|
1333
|
+
// boundary, so we defensively remove terminal controls here.
|
|
1334
|
+
const ESC_BYTE = 0x1b;
|
|
1335
|
+
const BEL_BYTE = 0x07;
|
|
1336
|
+
const ST_BYTE = 0x9c;
|
|
1337
|
+
|
|
1338
|
+
function isControlCharacter(code: number): boolean {
|
|
1339
|
+
return (code >= 0x00 && code <= 0x1f) || (code >= 0x7f && code <= 0x9f);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function isWhitespaceControl(code: number): boolean {
|
|
1343
|
+
return (
|
|
1344
|
+
code === 0x09 ||
|
|
1345
|
+
code === 0x0a ||
|
|
1346
|
+
code === 0x0b ||
|
|
1347
|
+
code === 0x0c ||
|
|
1348
|
+
code === 0x0d
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function skipCsiSequence(text: string, startIndex: number): number {
|
|
1353
|
+
for (let index = startIndex; index < text.length; index++) {
|
|
1354
|
+
const code = text.charCodeAt(index);
|
|
1355
|
+
if (code >= 0x40 && code <= 0x7e) return index + 1;
|
|
1356
|
+
}
|
|
1357
|
+
return text.length;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function skipStringControl(text: string, startIndex: number): number {
|
|
1361
|
+
for (let index = startIndex; index < text.length; index++) {
|
|
1362
|
+
const code = text.charCodeAt(index);
|
|
1363
|
+
if (code === BEL_BYTE || code === ST_BYTE) return index + 1;
|
|
1364
|
+
if (code === ESC_BYTE && text.charCodeAt(index + 1) === 0x5c) {
|
|
1365
|
+
return index + 2;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
return text.length;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function skipEscapeSequence(text: string, escapeIndex: number): number {
|
|
1372
|
+
const nextCode = text.charCodeAt(escapeIndex + 1);
|
|
1373
|
+
if (Number.isNaN(nextCode)) return escapeIndex + 1;
|
|
1374
|
+
|
|
1375
|
+
switch (nextCode) {
|
|
1376
|
+
case 0x5b: // CSI: ESC [
|
|
1377
|
+
return skipCsiSequence(text, escapeIndex + 2);
|
|
1378
|
+
case 0x5d: // OSC: ESC ]
|
|
1379
|
+
case 0x50: // DCS: ESC P
|
|
1380
|
+
case 0x58: // SOS: ESC X
|
|
1381
|
+
case 0x5e: // PM: ESC ^
|
|
1382
|
+
case 0x5f: // APC: ESC _
|
|
1383
|
+
return skipStringControl(text, escapeIndex + 2);
|
|
1384
|
+
default:
|
|
1385
|
+
if (
|
|
1386
|
+
nextCode === 0x20 ||
|
|
1387
|
+
nextCode === 0x23 ||
|
|
1388
|
+
nextCode === 0x25 ||
|
|
1389
|
+
(nextCode >= 0x28 && nextCode <= 0x2f)
|
|
1390
|
+
) {
|
|
1391
|
+
return Math.min(text.length, escapeIndex + 3);
|
|
1392
|
+
}
|
|
1393
|
+
return Math.min(text.length, escapeIndex + 2);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function stripTerminalControls(text: string): string {
|
|
1398
|
+
let stripped = "";
|
|
1399
|
+
for (let index = 0; index < text.length; ) {
|
|
1400
|
+
const code = text.charCodeAt(index);
|
|
1401
|
+
|
|
1402
|
+
if (code === ESC_BYTE) {
|
|
1403
|
+
index = skipEscapeSequence(text, index);
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
if (code === 0x9b) {
|
|
1407
|
+
index = skipCsiSequence(text, index + 1);
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (
|
|
1411
|
+
code === 0x90 ||
|
|
1412
|
+
code === 0x98 ||
|
|
1413
|
+
code === 0x9d ||
|
|
1414
|
+
code === 0x9e ||
|
|
1415
|
+
code === 0x9f
|
|
1416
|
+
) {
|
|
1417
|
+
index = skipStringControl(text, index + 1);
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
if (isControlCharacter(code)) {
|
|
1421
|
+
if (isWhitespaceControl(code)) stripped += " ";
|
|
1422
|
+
index++;
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
stripped += text[index];
|
|
1427
|
+
index++;
|
|
1428
|
+
}
|
|
1429
|
+
return stripped.replace(/\s+/gu, " ").trim();
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
// Second layer: strip leaked prompt scaffolding such as "Through activity 12:".
|
|
1433
|
+
// The system prompt forbids these prefixes, but model outputs sometimes still
|
|
1434
|
+
// parrot them; the regex is narrow enough not to clip natural starts like
|
|
1435
|
+
// "Activity slowed after retry" or "Checkpointing release state".
|
|
913
1436
|
const LEAKED_PREFIX_PATTERN =
|
|
914
1437
|
/^\s*(?:[-*•]\s*)?(?:(?:through\s+activity|activity|checkpoint)\s+\d+\s*[:.\-—–]\s*|(?:tldr|summary)\s*[:.\-—–]\s*)+/i;
|
|
915
|
-
|
|
916
1438
|
const LEADING_PUNCT_PATTERN = /^[\s\-—–•*:#.,;]+/;
|
|
917
1439
|
|
|
918
|
-
function
|
|
919
|
-
let cleaned = text
|
|
1440
|
+
function stripLeakedScaffolding(text: string): string {
|
|
1441
|
+
let cleaned = text;
|
|
920
1442
|
let previous: string;
|
|
921
1443
|
do {
|
|
922
1444
|
previous = cleaned;
|
|
923
1445
|
cleaned = cleaned.replace(LEAKED_PREFIX_PATTERN, "").trim();
|
|
924
1446
|
cleaned = cleaned.replace(LEADING_PUNCT_PATTERN, "").trim();
|
|
925
1447
|
} while (cleaned !== previous && cleaned.length > 0);
|
|
926
|
-
return cleaned
|
|
1448
|
+
return cleaned;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
function sanitizeTldrText(
|
|
1452
|
+
text: string,
|
|
1453
|
+
maxChars = MAX_SAFE_TLDR_CHARS,
|
|
1454
|
+
): string {
|
|
1455
|
+
const stripped = stripTerminalControls(text);
|
|
1456
|
+
const cleaned = stripLeakedScaffolding(stripped) || stripped;
|
|
1457
|
+
return truncateText(cleaned, maxChars);
|
|
927
1458
|
}
|
|
928
1459
|
|
|
929
1460
|
function shouldShowStatus(key: string, filter: StatusFilter): boolean {
|
|
@@ -1213,11 +1744,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
1213
1744
|
tldr.recordUserMessage(ctx, event.prompt);
|
|
1214
1745
|
});
|
|
1215
1746
|
pi.on("message_update", async (event, ctx) => {
|
|
1216
|
-
tldr.recordAssistantUpdate(
|
|
1747
|
+
tldr.recordAssistantUpdate(
|
|
1748
|
+
ctx,
|
|
1749
|
+
event.message,
|
|
1750
|
+
(event as { assistantMessageEvent?: unknown }).assistantMessageEvent,
|
|
1751
|
+
);
|
|
1217
1752
|
});
|
|
1218
1753
|
pi.on("tool_call", async (event, ctx) => {
|
|
1219
1754
|
tldr.recordToolCall(ctx, event);
|
|
1220
1755
|
});
|
|
1756
|
+
pi.on("tool_execution_update", async (event, ctx) => {
|
|
1757
|
+
tldr.recordToolExecutionUpdate(ctx, event);
|
|
1758
|
+
});
|
|
1221
1759
|
pi.on("tool_result", async (event, ctx) => {
|
|
1222
1760
|
tldr.recordToolResult(ctx, event);
|
|
1223
1761
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bar",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
54
|
+
"image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.25/assets/screenshot-yellow.png"
|
|
55
55
|
}
|
|
56
56
|
}
|