pi-bar 0.3.28 → 0.3.30
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 +16 -14
- package/extensions/status-footer.ts +311 -119
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -3,25 +3,25 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/pi-bar)
|
|
4
4
|
[](https://www.npmjs.com/package/pi-bar)
|
|
5
5
|
|
|
6
|
-
**Never accidentally run Opus on a typo again.** pi-bar keeps your model, thinking level, context pressure, extension status, and a live one-line
|
|
6
|
+
**Never accidentally run Opus on a typo again.** pi-bar keeps your model, thinking level, context pressure, extension status, and a live one-line progress update visible in pi's footer.
|
|
7
7
|
|
|
8
8
|
```text
|
|
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
|
|
|
18
18
|
- **See the active model at a glance** — catch accidental model switches before an expensive or sensitive task starts.
|
|
19
19
|
- **Track thinking level in place** — immediately notice when you are using the wrong reasoning setting.
|
|
20
20
|
- **Watch context pressure early** — context usage turns green, yellow, then red as you approach the limit.
|
|
21
|
-
- **Follow what pi is doing** — a live
|
|
21
|
+
- **Follow what pi is doing** — a live progress update keeps the current task visible without scrolling.
|
|
22
22
|
- **Keep extension statuses visible** — statuses set with `ctx.ui.setStatus()` still appear in the custom footer.
|
|
23
23
|
|
|
24
|
-
pi-bar is intentionally tiny: one small extension, no broad behavior changes, and no commands to learn. It replaces pi's built-in footer with a compact model / thinking / context /
|
|
24
|
+
pi-bar is intentionally tiny: one small extension, no broad behavior changes, and no commands to learn. It replaces pi's built-in footer with a compact model / thinking / context / progress / extension-status line.
|
|
25
25
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
@@ -42,23 +42,25 @@ pi-bar works out of the box, but you can tune it with environment variables befo
|
|
|
42
42
|
### Show or hide segments
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
|
-
PI_BAR_SHOW=model,thinking,context,
|
|
45
|
+
PI_BAR_SHOW=model,thinking,context,progress,extensions pi
|
|
46
46
|
PI_BAR_SHOW=model,context pi
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
Allowed segments are `model`, `thinking`, `context`, `
|
|
49
|
+
Allowed segments are `model`, `thinking`, `context`, `progress`, and `extensions`. The `progress` segment is hidden until pi-bar generates a current update. The `extensions` segment is hidden when no extension has set a status.
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
> The legacy `tldr` segment name and `PI_BAR_TLDR_MODEL` / `bar.tldrModel` settings still work and are kept as aliases for existing configs.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
### Configure live progress updates
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
pi-bar shows a concise live progress update of the current task in the footer. It generates updates from recent prompt, assistant, and deterministic code-compressed tool facts (for example `bash ... ok`, `grep ... 4 matches`, `read file`, or `custom_tool key=value ok`). It resets when you navigate the session tree so stale summaries do not follow you across branches. Set `PI_BAR_SHOW` without `progress` to hide it.
|
|
56
|
+
|
|
57
|
+
Override the progress update model with:
|
|
56
58
|
|
|
57
59
|
```bash
|
|
58
|
-
|
|
60
|
+
PI_BAR_PROGRESS_MODEL=anthropic/claude-haiku-4-5 pi
|
|
59
61
|
```
|
|
60
62
|
|
|
61
|
-
You can also set `bar.
|
|
63
|
+
You can also set `bar.progressModel` in pi settings.
|
|
62
64
|
|
|
63
65
|
### Configure extension statuses
|
|
64
66
|
|
|
@@ -90,4 +92,4 @@ The first number is the warning/yellow threshold. The second number is the dange
|
|
|
90
92
|
|
|
91
93
|
Pi extensions run with your local user permissions. Review any pi package source before installing it.
|
|
92
94
|
|
|
93
|
-
Live
|
|
95
|
+
Live progress update generation sends short activity snippets to the selected provider. Disable the `progress` segment with `PI_BAR_SHOW` if that is unacceptable.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* pi-bar — footer/statusline extension.
|
|
3
3
|
*
|
|
4
4
|
* Replaces the built-in footer with left-aligned segments:
|
|
5
|
-
* <model name> ❯ think:<level> ❯ <context% / window> ❯ <
|
|
5
|
+
* <model name> ❯ think:<level> ❯ <context% / window> ❯ <progress> ❯ <extension statuses>
|
|
6
6
|
*
|
|
7
7
|
* Examples:
|
|
8
8
|
* claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* and after each assistant turn so context usage stays current.
|
|
12
12
|
*
|
|
13
13
|
* Configuration env vars:
|
|
14
|
-
* PI_BAR_SHOW=model,thinking,context,
|
|
14
|
+
* PI_BAR_SHOW=model,thinking,context,progress,extensions
|
|
15
15
|
* PI_BAR_THRESHOLDS=70,90
|
|
16
16
|
*/
|
|
17
17
|
|
|
@@ -33,39 +33,39 @@ import {
|
|
|
33
33
|
truncateToWidth,
|
|
34
34
|
} from "@earendil-works/pi-tui";
|
|
35
35
|
|
|
36
|
-
type SegmentName = "model" | "thinking" | "context" | "
|
|
36
|
+
type SegmentName = "model" | "thinking" | "context" | "progress" | "extensions";
|
|
37
37
|
type StatusFilter =
|
|
38
38
|
| { mode: "all"; hidden: Set<string> }
|
|
39
39
|
| { mode: "only"; shown: Set<string> };
|
|
40
40
|
type SerializedStatusFilter =
|
|
41
41
|
| { mode: "all"; hidden: string[] }
|
|
42
42
|
| { mode: "only"; shown: string[] };
|
|
43
|
-
type
|
|
43
|
+
type ProgressActivityType =
|
|
44
44
|
| "user_message"
|
|
45
45
|
| "assistant_update"
|
|
46
46
|
| "tool_call"
|
|
47
47
|
| "tool_result"
|
|
48
48
|
| "assistant_final"
|
|
49
49
|
| "assistant_failure";
|
|
50
|
-
type
|
|
51
|
-
type
|
|
50
|
+
type ProgressDisplayPriority = "immediate" | "normal" | "final";
|
|
51
|
+
type ProgressActivity = {
|
|
52
52
|
index: number;
|
|
53
|
-
activityType:
|
|
54
|
-
displayPriority:
|
|
53
|
+
activityType: ProgressActivityType;
|
|
54
|
+
displayPriority: ProgressDisplayPriority;
|
|
55
55
|
text: string;
|
|
56
56
|
toolCallId?: string;
|
|
57
57
|
};
|
|
58
|
-
type
|
|
58
|
+
type ProgressCheckpoint = {
|
|
59
59
|
activityIndex: number;
|
|
60
|
-
displayPriority:
|
|
60
|
+
displayPriority: ProgressDisplayPriority;
|
|
61
61
|
text: string;
|
|
62
62
|
};
|
|
63
|
-
type
|
|
63
|
+
type ProgressCheckpointJob = {
|
|
64
64
|
activityIndex: number;
|
|
65
|
-
displayPriority:
|
|
65
|
+
displayPriority: ProgressDisplayPriority;
|
|
66
66
|
runId: number;
|
|
67
67
|
};
|
|
68
|
-
type
|
|
68
|
+
type ProgressModelPreference = { provider: string; id: string };
|
|
69
69
|
type FastModelAuth = {
|
|
70
70
|
model: Parameters<typeof complete>[0];
|
|
71
71
|
apiKey: string;
|
|
@@ -73,7 +73,10 @@ type FastModelAuth = {
|
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
const STATUS_FILTER_ENTRY_TYPE = "pi-bar-status-filter";
|
|
76
|
-
const
|
|
76
|
+
const SETTINGS_PROGRESS_KEY = "progress";
|
|
77
|
+
// Legacy settings/env names. The `tldr` keys remain accepted so existing user
|
|
78
|
+
// configs continue to work after the rename to "progress updates".
|
|
79
|
+
const LEGACY_SETTINGS_TLDR_KEY = "tldr";
|
|
77
80
|
const SETTINGS_BAR_KEY = "bar";
|
|
78
81
|
const MAX_ACTIVITY_TEXT_CHARS = 800;
|
|
79
82
|
const MAX_USER_TEXT_CHARS = 700;
|
|
@@ -84,10 +87,10 @@ const MAX_FINAL_TEXT_CHARS = 700;
|
|
|
84
87
|
const MAX_RETAINED_RAW_ACTIVITIES = 128;
|
|
85
88
|
const ANSI_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
86
89
|
const MAX_CONTEXT_CHECKPOINTS = 8;
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
90
|
+
const PROGRESS_MAX_TOKENS = 120;
|
|
91
|
+
const PROGRESS_REQUEST_TIMEOUT_MS = 2_000;
|
|
92
|
+
const PROGRESS_DISPLAY_UPDATE_INTERVAL_MS = 1_200;
|
|
93
|
+
const PROGRESS_TARGET_SUMMARY_CHARS = 60;
|
|
91
94
|
// Burst debouncing on the generation side: coalesce rapid-fire activities into
|
|
92
95
|
// one model call. Quiet window catches short bursts; max wait surfaces progress
|
|
93
96
|
// during continuous activity.
|
|
@@ -97,7 +100,7 @@ const NORMAL_CHECKPOINT_QUIET_MS = 1_500;
|
|
|
97
100
|
const NORMAL_CHECKPOINT_MAX_WAIT_MS = 2_500;
|
|
98
101
|
// Defensive ceiling: even if the model ignores the length instruction, never
|
|
99
102
|
// blast a large payload into the footer.
|
|
100
|
-
const
|
|
103
|
+
const MAX_SAFE_PROGRESS_CHARS = 240;
|
|
101
104
|
const CONFIG_PATH =
|
|
102
105
|
process.env.PI_BAR_CONFIG ?? join(homedir(), ".pi", "agent", "pi-bar.json");
|
|
103
106
|
|
|
@@ -105,7 +108,7 @@ const DEFAULT_SEGMENTS: SegmentName[] = [
|
|
|
105
108
|
"model",
|
|
106
109
|
"thinking",
|
|
107
110
|
"context",
|
|
108
|
-
"
|
|
111
|
+
"progress",
|
|
109
112
|
"extensions",
|
|
110
113
|
];
|
|
111
114
|
const DEFAULT_WARNING_THRESHOLD = 70;
|
|
@@ -168,11 +171,14 @@ function parseSegments(): SegmentName[] {
|
|
|
168
171
|
const raw = process.env.PI_BAR_SHOW;
|
|
169
172
|
if (!raw) return DEFAULT_SEGMENTS;
|
|
170
173
|
|
|
174
|
+
// `tldr` is the legacy segment name, kept working as an alias so existing
|
|
175
|
+
// PI_BAR_SHOW configs do not break after the rename to "progress".
|
|
171
176
|
const requested = raw
|
|
172
177
|
.split(",")
|
|
173
178
|
.map((segment) => segment.trim().toLowerCase())
|
|
179
|
+
.map((segment) => (segment === "tldr" ? "progress" : segment))
|
|
174
180
|
.filter((segment): segment is SegmentName =>
|
|
175
|
-
["model", "thinking", "context", "
|
|
181
|
+
["model", "thinking", "context", "progress", "extensions"].includes(segment),
|
|
176
182
|
);
|
|
177
183
|
|
|
178
184
|
return requested.length > 0 ? requested : DEFAULT_SEGMENTS;
|
|
@@ -206,7 +212,7 @@ function parseThresholds(): { warningThreshold: number; errorThreshold: number }
|
|
|
206
212
|
};
|
|
207
213
|
}
|
|
208
214
|
|
|
209
|
-
function
|
|
215
|
+
function parseProgressModelSpec(value: string): ProgressModelPreference | undefined {
|
|
210
216
|
const trimmed = value.trim();
|
|
211
217
|
if (!trimmed || trimmed === "auto") return undefined;
|
|
212
218
|
const separator = trimmed.indexOf("/");
|
|
@@ -217,51 +223,64 @@ function parseTldrModelSpec(value: string): TldrModelPreference | undefined {
|
|
|
217
223
|
function settingsModelValue(settings: Record<string, unknown>): string | undefined {
|
|
218
224
|
const bar = settings[SETTINGS_BAR_KEY];
|
|
219
225
|
if (bar && typeof bar === "object" && !Array.isArray(bar)) {
|
|
220
|
-
const
|
|
226
|
+
const record = bar as Record<string, unknown>;
|
|
227
|
+
// Canonical key after rename. Falls through to legacy `tldrModel` so old
|
|
228
|
+
// settings still work without manual migration.
|
|
229
|
+
if (typeof record.progressModel === "string") return record.progressModel;
|
|
230
|
+
if (typeof record.tldrModel === "string") return record.tldrModel;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const progressSection = settings[SETTINGS_PROGRESS_KEY];
|
|
234
|
+
if (progressSection && typeof progressSection === "object" && !Array.isArray(progressSection)) {
|
|
235
|
+
const value = (progressSection as Record<string, unknown>).model;
|
|
221
236
|
if (typeof value === "string") return value;
|
|
222
237
|
}
|
|
223
238
|
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
const value = (
|
|
239
|
+
const legacy = settings[LEGACY_SETTINGS_TLDR_KEY];
|
|
240
|
+
if (legacy && typeof legacy === "object" && !Array.isArray(legacy)) {
|
|
241
|
+
const value = (legacy as Record<string, unknown>).model;
|
|
227
242
|
if (typeof value === "string") return value;
|
|
228
243
|
}
|
|
229
244
|
|
|
230
245
|
return undefined;
|
|
231
246
|
}
|
|
232
247
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
248
|
+
function resolveProgressModelPreference(cwd: string): ProgressModelPreference | undefined {
|
|
249
|
+
// PI_BAR_PROGRESS_MODEL is the canonical env var. PI_BAR_TLDR_MODEL is
|
|
250
|
+
// honored as a backwards-compat alias for users who set it before the
|
|
251
|
+
// rename.
|
|
252
|
+
const envModel = process.env.PI_BAR_PROGRESS_MODEL ?? process.env.PI_BAR_TLDR_MODEL;
|
|
253
|
+
if (envModel) return parseProgressModelSpec(envModel);
|
|
236
254
|
|
|
237
255
|
const settings = SettingsManager.create(cwd);
|
|
238
256
|
const projectModel = settingsModelValue(
|
|
239
257
|
settings.getProjectSettings() as Record<string, unknown>,
|
|
240
258
|
);
|
|
241
|
-
if (projectModel !== undefined) return
|
|
259
|
+
if (projectModel !== undefined) return parseProgressModelSpec(projectModel);
|
|
242
260
|
|
|
243
261
|
const globalModel = settingsModelValue(
|
|
244
262
|
settings.getGlobalSettings() as Record<string, unknown>,
|
|
245
263
|
);
|
|
246
|
-
return globalModel ?
|
|
264
|
+
return globalModel ? parseProgressModelSpec(globalModel) : undefined;
|
|
247
265
|
}
|
|
248
266
|
|
|
249
|
-
// Fallback order optimizes for low latency + low cost first. The
|
|
250
|
-
// not need reasoning quality; codex/gpt-5.4-mini is fastest
|
|
251
|
-
|
|
267
|
+
// Fallback order optimizes for low latency + low cost first. The progress
|
|
268
|
+
// update path does not need reasoning quality; codex/gpt-5.4-mini is fastest
|
|
269
|
+
// in practice.
|
|
270
|
+
const FAST_PROGRESS_MODELS: readonly ProgressModelPreference[] = [
|
|
252
271
|
{ provider: "openai-codex", id: "gpt-5.4-mini" },
|
|
253
272
|
{ provider: "openai-codex", id: "gpt-5.3-codex-spark" },
|
|
254
273
|
{ provider: "anthropic", id: "claude-haiku-4-5" },
|
|
255
274
|
{ provider: "anthropic", id: "claude-haiku-4-5-20251001" },
|
|
256
275
|
];
|
|
257
276
|
|
|
258
|
-
function
|
|
277
|
+
function formatProgressModelKey(model: ProgressModelPreference): string {
|
|
259
278
|
return `${model.provider}/${model.id}`;
|
|
260
279
|
}
|
|
261
280
|
|
|
262
281
|
async function getModelAuth(
|
|
263
282
|
ctx: ExtensionContext,
|
|
264
|
-
preference:
|
|
283
|
+
preference: ProgressModelPreference,
|
|
265
284
|
): Promise<FastModelAuth | undefined> {
|
|
266
285
|
const model = ctx.modelRegistry.find(preference.provider, preference.id);
|
|
267
286
|
if (!model) return undefined;
|
|
@@ -272,17 +291,17 @@ async function getModelAuth(
|
|
|
272
291
|
: undefined;
|
|
273
292
|
}
|
|
274
293
|
|
|
275
|
-
async function
|
|
294
|
+
async function getFastProgressModelAuth(
|
|
276
295
|
ctx: ExtensionContext,
|
|
277
|
-
configuredModel?:
|
|
296
|
+
configuredModel?: ProgressModelPreference,
|
|
278
297
|
): Promise<FastModelAuth | undefined> {
|
|
279
298
|
if (configuredModel) {
|
|
280
299
|
const configuredAuth = await getModelAuth(ctx, configuredModel);
|
|
281
300
|
if (configuredAuth) return configuredAuth;
|
|
282
301
|
}
|
|
283
302
|
|
|
284
|
-
for (const candidate of
|
|
285
|
-
if (configuredModel &&
|
|
303
|
+
for (const candidate of FAST_PROGRESS_MODELS) {
|
|
304
|
+
if (configuredModel && formatProgressModelKey(candidate) === formatProgressModelKey(configuredModel)) {
|
|
286
305
|
continue;
|
|
287
306
|
}
|
|
288
307
|
const auth = await getModelAuth(ctx, candidate);
|
|
@@ -450,16 +469,16 @@ function extractTextContent(content: readonly unknown[] | undefined): string | u
|
|
|
450
469
|
return compacted.length > 0 ? compacted : undefined;
|
|
451
470
|
}
|
|
452
471
|
|
|
453
|
-
class
|
|
472
|
+
class ProgressFactCollector {
|
|
454
473
|
private nextIndex = 1;
|
|
455
|
-
private readonly activities:
|
|
474
|
+
private readonly activities: ProgressActivity[] = [];
|
|
456
475
|
|
|
457
476
|
resetConversation(): void {
|
|
458
477
|
this.nextIndex = 1;
|
|
459
478
|
this.activities.splice(0);
|
|
460
479
|
}
|
|
461
480
|
|
|
462
|
-
recordUserMessage(prompt: string):
|
|
481
|
+
recordUserMessage(prompt: string): ProgressActivity {
|
|
463
482
|
return this.addActivity(
|
|
464
483
|
"user_message",
|
|
465
484
|
"immediate",
|
|
@@ -467,7 +486,7 @@ class TldrFactCollector {
|
|
|
467
486
|
);
|
|
468
487
|
}
|
|
469
488
|
|
|
470
|
-
recordAssistantUpdate(message: unknown):
|
|
489
|
+
recordAssistantUpdate(message: unknown): ProgressActivity | undefined {
|
|
471
490
|
if (!message || typeof message !== "object") return undefined;
|
|
472
491
|
const record = message as Record<string, unknown>;
|
|
473
492
|
if (record.role !== "assistant") return undefined;
|
|
@@ -492,7 +511,7 @@ class TldrFactCollector {
|
|
|
492
511
|
toolName: string;
|
|
493
512
|
input?: Record<string, unknown>;
|
|
494
513
|
toolCallId?: string;
|
|
495
|
-
}):
|
|
514
|
+
}): ProgressActivity {
|
|
496
515
|
return this.addActivity(
|
|
497
516
|
"tool_call",
|
|
498
517
|
"normal",
|
|
@@ -508,7 +527,7 @@ class TldrFactCollector {
|
|
|
508
527
|
content?: readonly unknown[];
|
|
509
528
|
details?: unknown;
|
|
510
529
|
toolCallId?: string;
|
|
511
|
-
}):
|
|
530
|
+
}): ProgressActivity {
|
|
512
531
|
const isError = Boolean(event.isError);
|
|
513
532
|
const resultText = summarizeToolResult(
|
|
514
533
|
event.toolName,
|
|
@@ -540,7 +559,7 @@ class TldrFactCollector {
|
|
|
540
559
|
);
|
|
541
560
|
}
|
|
542
561
|
|
|
543
|
-
recordMessageEnd(message: unknown):
|
|
562
|
+
recordMessageEnd(message: unknown): ProgressActivity | "emptyFinalStop" | "ignored" {
|
|
544
563
|
if (!message || typeof message !== "object") return "ignored";
|
|
545
564
|
const record = message as Record<string, unknown>;
|
|
546
565
|
if (record.role !== "assistant") return "ignored";
|
|
@@ -569,7 +588,7 @@ class TldrFactCollector {
|
|
|
569
588
|
: "ignored";
|
|
570
589
|
}
|
|
571
590
|
|
|
572
|
-
activitiesAfter(previousIndex: number, throughIndex: number): readonly
|
|
591
|
+
activitiesAfter(previousIndex: number, throughIndex: number): readonly ProgressActivity[] {
|
|
573
592
|
return this.activities.filter(
|
|
574
593
|
(activity) => activity.index > previousIndex && activity.index <= throughIndex,
|
|
575
594
|
);
|
|
@@ -591,11 +610,11 @@ class TldrFactCollector {
|
|
|
591
610
|
}
|
|
592
611
|
|
|
593
612
|
private addActivity(
|
|
594
|
-
activityType:
|
|
595
|
-
displayPriority:
|
|
613
|
+
activityType: ProgressActivityType,
|
|
614
|
+
displayPriority: ProgressDisplayPriority,
|
|
596
615
|
text: string,
|
|
597
616
|
toolCallId?: string,
|
|
598
|
-
):
|
|
617
|
+
): ProgressActivity {
|
|
599
618
|
const activity = {
|
|
600
619
|
index: this.nextIndex,
|
|
601
620
|
activityType,
|
|
@@ -612,25 +631,25 @@ class TldrFactCollector {
|
|
|
612
631
|
}
|
|
613
632
|
}
|
|
614
633
|
|
|
615
|
-
class
|
|
616
|
-
private readonly facts = new
|
|
617
|
-
private configuredModel?:
|
|
634
|
+
class FooterProgressEngine {
|
|
635
|
+
private readonly facts = new ProgressFactCollector();
|
|
636
|
+
private configuredModel?: ProgressModelPreference;
|
|
618
637
|
private runId = 0;
|
|
619
638
|
private latestAcceptedActivityIndex = 0;
|
|
620
639
|
private lastRenderedActivityIndex = 0;
|
|
621
640
|
private lastRenderedText = "";
|
|
622
641
|
private lastDisplayAt = Number.NEGATIVE_INFINITY;
|
|
623
|
-
private readonly acceptedCheckpoints:
|
|
624
|
-
private checkpointQueue:
|
|
625
|
-
private inFlightCheckpoint?:
|
|
626
|
-
private pendingDisplayCheckpoint?:
|
|
642
|
+
private readonly acceptedCheckpoints: ProgressCheckpoint[] = [];
|
|
643
|
+
private checkpointQueue: ProgressCheckpointJob[] = [];
|
|
644
|
+
private inFlightCheckpoint?: ProgressCheckpointJob;
|
|
645
|
+
private pendingDisplayCheckpoint?: ProgressCheckpoint;
|
|
627
646
|
private displayTimer?: ReturnType<typeof setTimeout>;
|
|
628
647
|
private abortController?: AbortController;
|
|
629
648
|
private currentText: string | null = null;
|
|
630
649
|
// Normal-priority bursts wait for a quiet window before triggering a model
|
|
631
650
|
// call. Pi often emits many rapid activities (e.g. streaming reads); without
|
|
632
|
-
// debouncing we would hammer the
|
|
633
|
-
private pendingNormalCheckpoint?:
|
|
651
|
+
// debouncing we would hammer the progress update model for intermediate states.
|
|
652
|
+
private pendingNormalCheckpoint?: ProgressCheckpointJob;
|
|
634
653
|
private normalCheckpointTimer?: ReturnType<typeof setTimeout>;
|
|
635
654
|
private normalCheckpointBurstStartedAt?: number;
|
|
636
655
|
|
|
@@ -641,7 +660,7 @@ class FooterTldrEngine {
|
|
|
641
660
|
}
|
|
642
661
|
|
|
643
662
|
startSession(cwd: string): void {
|
|
644
|
-
this.configuredModel =
|
|
663
|
+
this.configuredModel = resolveProgressModelPreference(cwd);
|
|
645
664
|
this.facts.resetConversation();
|
|
646
665
|
this.startFreshRun();
|
|
647
666
|
}
|
|
@@ -652,7 +671,7 @@ class FooterTldrEngine {
|
|
|
652
671
|
}
|
|
653
672
|
|
|
654
673
|
recordUserMessage(ctx: ExtensionContext, prompt: string): void {
|
|
655
|
-
// A new user turn replaces the entire visible
|
|
674
|
+
// A new user turn replaces the entire visible progress update context, so wipe both
|
|
656
675
|
// the rendered text and the prior-turn checkpoint context that feeds the
|
|
657
676
|
// model. Carrying old checkpoints biased the next turn's phrasing toward
|
|
658
677
|
// the previous task.
|
|
@@ -766,7 +785,7 @@ class FooterTldrEngine {
|
|
|
766
785
|
|
|
767
786
|
private scheduleNormalCheckpoint(
|
|
768
787
|
ctx: ExtensionContext,
|
|
769
|
-
job:
|
|
788
|
+
job: ProgressCheckpointJob,
|
|
770
789
|
): void {
|
|
771
790
|
this.pendingNormalCheckpoint = job;
|
|
772
791
|
this.normalCheckpointBurstStartedAt ??= performance.now();
|
|
@@ -804,7 +823,7 @@ class FooterTldrEngine {
|
|
|
804
823
|
this.pump(ctx);
|
|
805
824
|
}
|
|
806
825
|
|
|
807
|
-
private enqueue(ctx: ExtensionContext, activity:
|
|
826
|
+
private enqueue(ctx: ExtensionContext, activity: ProgressActivity): void {
|
|
808
827
|
if (!ctx.hasUI) return;
|
|
809
828
|
const job = {
|
|
810
829
|
activityIndex: activity.index,
|
|
@@ -868,19 +887,19 @@ class FooterTldrEngine {
|
|
|
868
887
|
void this.runCheckpointRequest(ctx, job);
|
|
869
888
|
}
|
|
870
889
|
|
|
871
|
-
private isCurrentJob(job:
|
|
890
|
+
private isCurrentJob(job: ProgressCheckpointJob): boolean {
|
|
872
891
|
return job.runId === this.runId && this.inFlightCheckpoint === job;
|
|
873
892
|
}
|
|
874
893
|
|
|
875
894
|
private async runCheckpointRequest(
|
|
876
895
|
ctx: ExtensionContext,
|
|
877
|
-
job:
|
|
896
|
+
job: ProgressCheckpointJob,
|
|
878
897
|
): Promise<void> {
|
|
879
898
|
let abortController: AbortController | undefined;
|
|
880
899
|
try {
|
|
881
900
|
const prompt = this.checkpointPrompt(job);
|
|
882
901
|
if (!prompt) return;
|
|
883
|
-
const auth = await
|
|
902
|
+
const auth = await getFastProgressModelAuth(ctx, this.configuredModel);
|
|
884
903
|
if (!this.isCurrentJob(job) || !auth) return;
|
|
885
904
|
|
|
886
905
|
abortController = new AbortController();
|
|
@@ -891,17 +910,17 @@ class FooterTldrEngine {
|
|
|
891
910
|
{
|
|
892
911
|
apiKey: auth.apiKey,
|
|
893
912
|
headers: auth.headers,
|
|
894
|
-
maxTokens:
|
|
913
|
+
maxTokens: PROGRESS_MAX_TOKENS,
|
|
895
914
|
maxRetries: 0,
|
|
896
915
|
cacheRetention: "none",
|
|
897
|
-
timeoutMs:
|
|
916
|
+
timeoutMs: PROGRESS_REQUEST_TIMEOUT_MS,
|
|
898
917
|
signal: abortController.signal,
|
|
899
918
|
},
|
|
900
919
|
);
|
|
901
920
|
if (!this.isCurrentJob(job) || response.stopReason !== "stop") return;
|
|
902
921
|
|
|
903
922
|
const rawText = extractTextContent(response.content) ?? "";
|
|
904
|
-
const text =
|
|
923
|
+
const text = sanitizeProgressText(rawText);
|
|
905
924
|
if (!text) return;
|
|
906
925
|
const checkpoint = {
|
|
907
926
|
activityIndex: job.activityIndex,
|
|
@@ -921,7 +940,7 @@ class FooterTldrEngine {
|
|
|
921
940
|
}
|
|
922
941
|
}
|
|
923
942
|
|
|
924
|
-
private acceptCheckpoint(checkpoint:
|
|
943
|
+
private acceptCheckpoint(checkpoint: ProgressCheckpoint): void {
|
|
925
944
|
this.acceptedCheckpoints.push(checkpoint);
|
|
926
945
|
if (this.acceptedCheckpoints.length > MAX_CONTEXT_CHECKPOINTS) {
|
|
927
946
|
this.acceptedCheckpoints.splice(
|
|
@@ -935,7 +954,7 @@ class FooterTldrEngine {
|
|
|
935
954
|
|
|
936
955
|
private considerDisplayingCheckpoint(
|
|
937
956
|
ctx: ExtensionContext,
|
|
938
|
-
checkpoint:
|
|
957
|
+
checkpoint: ProgressCheckpoint,
|
|
939
958
|
): void {
|
|
940
959
|
if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
|
|
941
960
|
if (checkpoint.activityIndex !== this.facts.latestActivityIndex()) return;
|
|
@@ -947,7 +966,7 @@ class FooterTldrEngine {
|
|
|
947
966
|
}
|
|
948
967
|
|
|
949
968
|
const elapsedMs = performance.now() - this.lastDisplayAt;
|
|
950
|
-
if (elapsedMs >=
|
|
969
|
+
if (elapsedMs >= PROGRESS_DISPLAY_UPDATE_INTERVAL_MS) {
|
|
951
970
|
this.clearPendingDisplay();
|
|
952
971
|
this.renderCheckpoint(checkpoint);
|
|
953
972
|
return;
|
|
@@ -961,16 +980,16 @@ class FooterTldrEngine {
|
|
|
961
980
|
this.pendingDisplayCheckpoint = undefined;
|
|
962
981
|
if (!pending || pending.activityIndex !== this.facts.latestActivityIndex()) return;
|
|
963
982
|
this.renderCheckpoint(pending);
|
|
964
|
-
},
|
|
983
|
+
}, PROGRESS_DISPLAY_UPDATE_INTERVAL_MS - elapsedMs);
|
|
965
984
|
}
|
|
966
985
|
|
|
967
|
-
private renderCheckpoint(checkpoint:
|
|
986
|
+
private renderCheckpoint(checkpoint: ProgressCheckpoint): void {
|
|
968
987
|
if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
|
|
969
988
|
if (checkpoint.text === this.lastRenderedText) return;
|
|
970
989
|
// Skip near-duplicates: a burst of consecutive checkpoints often produces
|
|
971
990
|
// the same action fragment with only punctuation/casing differences. They
|
|
972
991
|
// flash visibly in the footer without adding information.
|
|
973
|
-
if (
|
|
992
|
+
if (isNearDuplicateProgress(checkpoint.text, this.lastRenderedText)) {
|
|
974
993
|
this.lastRenderedActivityIndex = checkpoint.activityIndex;
|
|
975
994
|
return;
|
|
976
995
|
}
|
|
@@ -981,7 +1000,7 @@ class FooterTldrEngine {
|
|
|
981
1000
|
this.requestRender();
|
|
982
1001
|
}
|
|
983
1002
|
|
|
984
|
-
private checkpointPrompt(job:
|
|
1003
|
+
private checkpointPrompt(job: ProgressCheckpointJob): UserMessage | undefined {
|
|
985
1004
|
const rawActivities = this.facts.activitiesAfter(
|
|
986
1005
|
this.latestAcceptedActivityIndex,
|
|
987
1006
|
job.activityIndex,
|
|
@@ -993,13 +1012,13 @@ class FooterTldrEngine {
|
|
|
993
1012
|
{
|
|
994
1013
|
type: "text",
|
|
995
1014
|
text: [
|
|
996
|
-
"Prior
|
|
1015
|
+
"Prior progress updates (context only, do not copy phrasing):",
|
|
997
1016
|
previousCheckpointLines(this.acceptedCheckpoints),
|
|
998
1017
|
"",
|
|
999
1018
|
"New activity to summarize:",
|
|
1000
1019
|
...rawActivities.map(formatRawActivity),
|
|
1001
1020
|
"",
|
|
1002
|
-
"Write the next
|
|
1021
|
+
"Write the next progress update.",
|
|
1003
1022
|
].join("\n"),
|
|
1004
1023
|
},
|
|
1005
1024
|
],
|
|
@@ -1008,60 +1027,149 @@ class FooterTldrEngine {
|
|
|
1008
1027
|
}
|
|
1009
1028
|
}
|
|
1010
1029
|
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1030
|
+
// First-word allow/ban lists are duplicated between the prompt and the
|
|
1031
|
+
// sanitizer fallback. They live here so both stay aligned automatically when
|
|
1032
|
+
// new verbs get added or moved.
|
|
1033
|
+
const ALLOWED_FIRST_WORDS_PROGRESSIVE = [
|
|
1034
|
+
"Reviewing",
|
|
1035
|
+
"Investigating",
|
|
1036
|
+
"Exploring",
|
|
1037
|
+
"Updating",
|
|
1038
|
+
"Refining",
|
|
1039
|
+
"Fixing",
|
|
1040
|
+
"Implementing",
|
|
1041
|
+
"Wrapping up",
|
|
1042
|
+
"Bumping",
|
|
1043
|
+
"Releasing",
|
|
1044
|
+
"Preparing",
|
|
1045
|
+
"Drafting",
|
|
1046
|
+
"Resuming",
|
|
1047
|
+
"Pulling",
|
|
1048
|
+
"Surveying",
|
|
1049
|
+
"Recording",
|
|
1050
|
+
] as const;
|
|
1051
|
+
const ALLOWED_FIRST_WORDS_PAST = [
|
|
1052
|
+
"Reviewed",
|
|
1053
|
+
"Investigated",
|
|
1054
|
+
"Explored",
|
|
1055
|
+
"Updated",
|
|
1056
|
+
"Refined",
|
|
1057
|
+
"Fixed",
|
|
1058
|
+
"Implemented",
|
|
1059
|
+
"Wrapped up",
|
|
1060
|
+
"Bumped",
|
|
1061
|
+
"Released",
|
|
1062
|
+
"Prepared",
|
|
1063
|
+
"Drafted",
|
|
1064
|
+
"Resumed",
|
|
1065
|
+
"Pulled",
|
|
1066
|
+
"Surveyed",
|
|
1067
|
+
"Recorded",
|
|
1068
|
+
] as const;
|
|
1069
|
+
const BANNED_FIRST_WORDS = [
|
|
1070
|
+
"Read",
|
|
1071
|
+
"Reading",
|
|
1072
|
+
"Grep",
|
|
1073
|
+
"Grepping",
|
|
1074
|
+
"Listing",
|
|
1075
|
+
"List",
|
|
1076
|
+
"Counting",
|
|
1077
|
+
"Counted",
|
|
1078
|
+
"Extracting",
|
|
1079
|
+
"Extracted",
|
|
1080
|
+
"Displaying",
|
|
1081
|
+
"Displayed",
|
|
1082
|
+
"Editing",
|
|
1083
|
+
"Edited",
|
|
1084
|
+
"Writing",
|
|
1085
|
+
"Wrote",
|
|
1086
|
+
"Running",
|
|
1087
|
+
"Ran",
|
|
1088
|
+
"Publishing",
|
|
1089
|
+
"Published",
|
|
1090
|
+
"Capturing",
|
|
1091
|
+
"Captured",
|
|
1092
|
+
"Verifying",
|
|
1093
|
+
"Verified",
|
|
1094
|
+
"Verify",
|
|
1095
|
+
"Validating",
|
|
1096
|
+
"Validated",
|
|
1097
|
+
"Validate",
|
|
1098
|
+
"Checking",
|
|
1099
|
+
"Checked",
|
|
1100
|
+
"Check",
|
|
1101
|
+
"Confirming",
|
|
1102
|
+
"Confirmed",
|
|
1103
|
+
"Confirm",
|
|
1104
|
+
"Searching",
|
|
1105
|
+
"Searched",
|
|
1106
|
+
"Search",
|
|
1107
|
+
"Finding",
|
|
1108
|
+
"Found",
|
|
1109
|
+
"Find",
|
|
1110
|
+
] as const;
|
|
1111
|
+
|
|
1112
|
+
function checkpointSystemPrompt(job: ProgressCheckpointJob): string {
|
|
1113
|
+
// Tense + exemplars vary by priority. Final progress updates describe
|
|
1114
|
+
// completed turns; using -ing examples for final caused all of them to slip
|
|
1115
|
+
// back into present-progressive even though the literal instruction said
|
|
1116
|
+
// past tense.
|
|
1117
|
+
// Immediate progress updates cover a brand-new user request; they need a rephrase
|
|
1016
1118
|
// directive instead of generic filler like "Continuing with task".
|
|
1017
1119
|
let tenseInstruction: string;
|
|
1018
1120
|
let goodExamples: string;
|
|
1121
|
+
let allowedFirstWords: readonly string[];
|
|
1019
1122
|
if (job.displayPriority === "final") {
|
|
1020
1123
|
tenseInstruction = "Start with a past-tense verb describing what was completed.";
|
|
1021
1124
|
goodExamples = [
|
|
1022
1125
|
"- Updated footer summary behavior",
|
|
1023
|
-
"- Investigated live
|
|
1126
|
+
"- Investigated live progress regressions",
|
|
1024
1127
|
"- Refined sanitizer for stray prefixes",
|
|
1025
1128
|
"- Wrapped up extension release",
|
|
1026
1129
|
].join("\n");
|
|
1130
|
+
allowedFirstWords = ALLOWED_FIRST_WORDS_PAST;
|
|
1027
1131
|
} else if (job.displayPriority === "immediate") {
|
|
1028
1132
|
tenseInstruction =
|
|
1029
|
-
"Rephrase the user's new request as a concise present-progressive task clause.
|
|
1133
|
+
"Rephrase the user's new request as a concise present-progressive task clause. If the request is opaque (e.g. \"continue\", \"go\", \"ok\"), name the carry-over task with a noun (e.g. \"Resuming refactor work\"), not generic filler.";
|
|
1030
1134
|
goodExamples = [
|
|
1031
1135
|
"- Reviewing footer summary behavior",
|
|
1032
|
-
"- Investigating live
|
|
1136
|
+
"- Investigating live progress regressions",
|
|
1033
1137
|
"- Refining sanitizer for stray prefixes",
|
|
1034
1138
|
"- Preparing extension release",
|
|
1139
|
+
"- Resuming refactor work",
|
|
1035
1140
|
].join("\n");
|
|
1141
|
+
allowedFirstWords = ALLOWED_FIRST_WORDS_PROGRESSIVE;
|
|
1036
1142
|
} else {
|
|
1037
1143
|
tenseInstruction = "Start with a present-tense -ing verb describing current work.";
|
|
1038
1144
|
goodExamples = [
|
|
1039
1145
|
"- Reviewing footer summary behavior",
|
|
1040
|
-
"- Investigating live
|
|
1146
|
+
"- Investigating live progress regressions",
|
|
1041
1147
|
"- Refining sanitizer for stray prefixes",
|
|
1042
1148
|
"- Wrapping up extension release",
|
|
1043
1149
|
].join("\n");
|
|
1150
|
+
allowedFirstWords = ALLOWED_FIRST_WORDS_PROGRESSIVE;
|
|
1044
1151
|
}
|
|
1045
1152
|
|
|
1046
1153
|
// Frame the model as a human developer describing visible work, not an agent
|
|
1047
1154
|
// summarizing its own mechanics. The banned phrases and few-shot examples
|
|
1048
1155
|
// fix concrete regressions observed in backtests: tool-name verbs, success
|
|
1049
|
-
// suffixes, file path leaks, and markdown formatting.
|
|
1050
|
-
|
|
1156
|
+
// suffixes, file path leaks, and markdown formatting. The HARD CONSTRAINTS
|
|
1157
|
+
// block sits at the tail of the prompt because instruction-following models
|
|
1158
|
+
// give the most weight to the most-recent directives (recency bias).
|
|
1159
|
+
return `Write one plain-English progress update for a Pi coding agent.
|
|
1051
1160
|
Describe the work progress as if a human developer were doing it.
|
|
1052
1161
|
Focus on the task activity and current outcome, not agent mechanics.
|
|
1053
1162
|
Do not mention tools, tool calls, prompts, messages, model output, or implementation details.
|
|
1054
|
-
|
|
1055
|
-
Use human-developer verbs instead: Reviewing, Investigating, Exploring, Updating, Refining, Fixing, Implementing, Wrapping up, Bumping, Releasing.
|
|
1163
|
+
Use human-developer verbs instead of tool-narration verbs.
|
|
1056
1164
|
Do not use file paths, file extensions, code identifiers, package names, or version strings.
|
|
1057
1165
|
Do not use backticks, asterisks, underscores, quotes, or any markdown formatting.
|
|
1058
1166
|
Do not append filler suffixes such as "with success", "successfully", or "completed successfully".
|
|
1059
1167
|
Do not claim progress or completion that is not present in the activity.
|
|
1060
|
-
Use the prior
|
|
1168
|
+
Use the prior progress updates for context and the new activity for the update.
|
|
1061
1169
|
Summarize the current state of work; do not narrate the history.
|
|
1062
1170
|
If context is sparse, still summarize the available activity.
|
|
1063
1171
|
Never ask for more information or say there is not enough context.
|
|
1064
|
-
Return one concise status fragment under ${
|
|
1172
|
+
Return one concise status fragment under ${PROGRESS_TARGET_SUMMARY_CHARS} characters.
|
|
1065
1173
|
Omit subjects like "the agent" or "it".
|
|
1066
1174
|
Prefer verb + direct object. Include outcome only if important.
|
|
1067
1175
|
Do not address the user.
|
|
@@ -1075,11 +1183,14 @@ Bad examples:
|
|
|
1075
1183
|
- Editing extensions/status-footer.ts with success.
|
|
1076
1184
|
- Reading status-footer file completed successfully.
|
|
1077
1185
|
- Publishing \`pi-bar@0.3.3\` to npm.
|
|
1078
|
-
- Grepping for
|
|
1079
|
-
-
|
|
1186
|
+
- Grepping for sanitizeProgressText callers.
|
|
1187
|
+
- Verifying repository status after commit.
|
|
1080
1188
|
- Investigating user input responses.
|
|
1081
1189
|
|
|
1082
|
-
|
|
1190
|
+
HARD CONSTRAINTS (apply last; override anything above that conflicts):
|
|
1191
|
+
- First word MUST be one of: ${allowedFirstWords.join(", ")}.
|
|
1192
|
+
- First word MUST NOT be: ${BANNED_FIRST_WORDS.join(", ")}.
|
|
1193
|
+
- ${tenseInstruction}`;
|
|
1083
1194
|
}
|
|
1084
1195
|
|
|
1085
1196
|
function extractStopReason(message: unknown): string | undefined {
|
|
@@ -1089,7 +1200,7 @@ function extractStopReason(message: unknown): string | undefined {
|
|
|
1089
1200
|
return typeof stopReason === "string" ? stopReason : undefined;
|
|
1090
1201
|
}
|
|
1091
1202
|
|
|
1092
|
-
// Two
|
|
1203
|
+
// Two progress updates are near-duplicates if their normalized action fragments (lowercase,
|
|
1093
1204
|
// punctuation/whitespace collapsed) match. Catches bursts of cosmetic variants
|
|
1094
1205
|
// like "Editing X." vs "Editing X" vs "editing x.".
|
|
1095
1206
|
function normalizedActionFragment(text: string): string {
|
|
@@ -1100,25 +1211,25 @@ function normalizedActionFragment(text: string): string {
|
|
|
1100
1211
|
.trim();
|
|
1101
1212
|
}
|
|
1102
1213
|
|
|
1103
|
-
function
|
|
1214
|
+
function isNearDuplicateProgress(current: string, previous: string): boolean {
|
|
1104
1215
|
if (!previous) return false;
|
|
1105
1216
|
return normalizedActionFragment(current) === normalizedActionFragment(previous);
|
|
1106
1217
|
}
|
|
1107
1218
|
|
|
1108
|
-
function previousCheckpointLines(checkpoints: readonly
|
|
1219
|
+
function previousCheckpointLines(checkpoints: readonly ProgressCheckpoint[]): string {
|
|
1109
1220
|
if (checkpoints.length === 0) return "none";
|
|
1110
1221
|
return checkpoints
|
|
1111
1222
|
.slice(-MAX_CONTEXT_CHECKPOINTS)
|
|
1112
|
-
.map((checkpoint) => `- ${
|
|
1223
|
+
.map((checkpoint) => `- ${sanitizeProgressText(checkpoint.text)}`)
|
|
1113
1224
|
.join("\n");
|
|
1114
1225
|
}
|
|
1115
1226
|
|
|
1116
|
-
function formatRawActivity(activity:
|
|
1227
|
+
function formatRawActivity(activity: ProgressActivity): string {
|
|
1117
1228
|
return `- ${activity.text}`;
|
|
1118
1229
|
}
|
|
1119
1230
|
|
|
1120
1231
|
// Strips ANSI/CSI/OSC/DCS/SOS/PM/APC escape sequences from model output before
|
|
1121
|
-
// it lands in the terminal. The
|
|
1232
|
+
// it lands in the terminal. The progress update text is model-controlled and renders into
|
|
1122
1233
|
// the footer; the system prompt asking for plain text is not a security
|
|
1123
1234
|
// boundary, so we defensively remove terminal controls here.
|
|
1124
1235
|
const ESC_BYTE = 0x1b;
|
|
@@ -1224,7 +1335,7 @@ function stripTerminalControls(text: string): string {
|
|
|
1224
1335
|
// parrot them; the regex is narrow enough not to clip natural starts like
|
|
1225
1336
|
// "Activity slowed after retry" or "Checkpointing release state".
|
|
1226
1337
|
const LEAKED_PREFIX_PATTERN =
|
|
1227
|
-
/^\s*(?:[-*•]\s*)?(?:(?:through\s+activity|activity|checkpoint)\s+\d+\s*[:.\-—–]\s*|(?:tldr|summary)\s*[:.\-—–]\s*)+/i;
|
|
1338
|
+
/^\s*(?:[-*•]\s*)?(?:(?:through\s+activity|activity|checkpoint)\s+\d+\s*[:.\-—–]\s*|(?:tldr|summary|progress\s+update|progress)\s*[:.\-—–]\s*)+/i;
|
|
1228
1339
|
const LEADING_PUNCT_PATTERN = /^[\s\-—–•*:#.,;]+/;
|
|
1229
1340
|
const TRAILING_PUNCT_PATTERN = /[\s\-—–•*:#.,;]+$/;
|
|
1230
1341
|
|
|
@@ -1273,18 +1384,98 @@ const DANGLING_TRAILING_PREP_PATTERN =
|
|
|
1273
1384
|
/\s+(?:to|at|as|of|by|for|in|on|with|from|version|v)\s*[.!?,;:]?\s*$/i;
|
|
1274
1385
|
const DANGLING_PREP_CHAIN_PATTERN =
|
|
1275
1386
|
/\b(to|at|as|of|by|from|version|v)\s+(for|in|on|with|from|after|before|during|to|at|as|of|by|and|but|or)\b/gi;
|
|
1387
|
+
// File-path / identifier strip can leave a verb directly followed by a
|
|
1388
|
+
// preposition: "Reviewing README for image update" -> after path strip ->
|
|
1389
|
+
// "Reviewing for image update". Collapse the bare verb+prep into just the verb.
|
|
1390
|
+
const VERB_BARE_PREP_PATTERN =
|
|
1391
|
+
/\b(Reviewing|Investigating|Updating|Refining|Exploring|Fixing|Implementing|Bumping|Releasing|Preparing|Drafting|Resuming|Pulling|Surveying|Recording)\s+(?:for|in|on|with|after|before|to|at|as|of|by|from)\s+/gi;
|
|
1392
|
+
// Identifier strip removes version numbers like "0.3.4" but can leave stranded
|
|
1393
|
+
// release adjectives behind: "Released latest of package" was "Released latest
|
|
1394
|
+
// 0.3.4 of package"; "Released new with X" was "Released new 0.3.4 with X".
|
|
1395
|
+
// The chain pattern keeps the noun phrase that followed the version. The
|
|
1396
|
+
// trailing pattern handles bare "... new" or "... latest" tails.
|
|
1397
|
+
const RELEASE_FRAGMENT_CHAIN_PATTERN =
|
|
1398
|
+
/\b(Released|Releasing|Bumped|Bumping|Published|Publishing|Updated|Updating|Shipped|Shipping)\s+(?:new|latest|version|update)\s+(?:of|with|to|and|in|on|for)\s+/gi;
|
|
1399
|
+
const RELEASE_FRAGMENT_TRAILING_PATTERN =
|
|
1400
|
+
/\s+(?:new|latest|version|update)\s*(?:of|with|to|and|in|on)?\s*[.!?,;:]?\s*$/i;
|
|
1276
1401
|
|
|
1277
1402
|
function stripDanglingPrepositions(text: string): string {
|
|
1278
1403
|
let cleaned = text;
|
|
1279
1404
|
let previous: string;
|
|
1280
1405
|
do {
|
|
1281
1406
|
previous = cleaned;
|
|
1407
|
+
cleaned = cleaned.replace(VERB_BARE_PREP_PATTERN, "$1 ");
|
|
1408
|
+
cleaned = cleaned.replace(RELEASE_FRAGMENT_CHAIN_PATTERN, "$1 ");
|
|
1282
1409
|
cleaned = cleaned.replace(DANGLING_PREP_CHAIN_PATTERN, "$2");
|
|
1410
|
+
cleaned = cleaned.replace(RELEASE_FRAGMENT_TRAILING_PATTERN, "").trim();
|
|
1283
1411
|
cleaned = cleaned.replace(DANGLING_TRAILING_PREP_PATTERN, "").trim();
|
|
1284
1412
|
} while (cleaned !== previous && cleaned.length > 0);
|
|
1285
1413
|
return cleaned;
|
|
1286
1414
|
}
|
|
1287
1415
|
|
|
1416
|
+
// Last-resort fallback when the model ignored the allow-list and started the
|
|
1417
|
+
// progress update with a banned tool-narration verb. The map preserves tense (present
|
|
1418
|
+
// vs past). Case of the first letter follows the original word.
|
|
1419
|
+
const BANNED_FIRST_WORD_REWRITES: Record<string, string> = {
|
|
1420
|
+
Reading: "Reviewing",
|
|
1421
|
+
Read: "Reviewed",
|
|
1422
|
+
Grepping: "Investigating",
|
|
1423
|
+
Grep: "Investigated",
|
|
1424
|
+
Listing: "Reviewing",
|
|
1425
|
+
List: "Reviewed",
|
|
1426
|
+
Counting: "Surveying",
|
|
1427
|
+
Counted: "Surveyed",
|
|
1428
|
+
Extracting: "Pulling",
|
|
1429
|
+
Extracted: "Pulled",
|
|
1430
|
+
Displaying: "Reviewing",
|
|
1431
|
+
Displayed: "Reviewed",
|
|
1432
|
+
Editing: "Updating",
|
|
1433
|
+
Edited: "Updated",
|
|
1434
|
+
Writing: "Drafting",
|
|
1435
|
+
Wrote: "Drafted",
|
|
1436
|
+
Running: "Working on",
|
|
1437
|
+
Ran: "Worked on",
|
|
1438
|
+
Publishing: "Releasing",
|
|
1439
|
+
Published: "Released",
|
|
1440
|
+
Capturing: "Recording",
|
|
1441
|
+
Captured: "Recorded",
|
|
1442
|
+
Verifying: "Reviewing",
|
|
1443
|
+
Verified: "Reviewed",
|
|
1444
|
+
Verify: "Review",
|
|
1445
|
+
Validating: "Reviewing",
|
|
1446
|
+
Validated: "Reviewed",
|
|
1447
|
+
Validate: "Review",
|
|
1448
|
+
Checking: "Reviewing",
|
|
1449
|
+
Checked: "Reviewed",
|
|
1450
|
+
Check: "Review",
|
|
1451
|
+
Confirming: "Reviewing",
|
|
1452
|
+
Confirmed: "Reviewed",
|
|
1453
|
+
Confirm: "Review",
|
|
1454
|
+
Searching: "Investigating",
|
|
1455
|
+
Searched: "Investigated",
|
|
1456
|
+
Search: "Investigate",
|
|
1457
|
+
Finding: "Investigating",
|
|
1458
|
+
Found: "Investigated",
|
|
1459
|
+
Find: "Investigate",
|
|
1460
|
+
};
|
|
1461
|
+
const BANNED_FIRST_WORD_PATTERN = new RegExp(
|
|
1462
|
+
`^(${Object.keys(BANNED_FIRST_WORD_REWRITES).join("|")})\\b`,
|
|
1463
|
+
);
|
|
1464
|
+
|
|
1465
|
+
function rewriteBannedFirstWord(text: string): string {
|
|
1466
|
+
const match = BANNED_FIRST_WORD_PATTERN.exec(text);
|
|
1467
|
+
if (!match) return text;
|
|
1468
|
+
const original = match[1];
|
|
1469
|
+
const replacement = BANNED_FIRST_WORD_REWRITES[original];
|
|
1470
|
+
if (!replacement) return text;
|
|
1471
|
+
// Preserve lowercase-start outputs (rare but possible) by matching original case.
|
|
1472
|
+
const cased =
|
|
1473
|
+
original[0] === original[0].toLowerCase()
|
|
1474
|
+
? replacement[0].toLowerCase() + replacement.slice(1)
|
|
1475
|
+
: replacement;
|
|
1476
|
+
return cased + text.slice(original.length);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1288
1479
|
// Trailing filler suffixes observed in backtests:
|
|
1289
1480
|
// "... with success.", "... completed successfully.", "... successfully."
|
|
1290
1481
|
const SUCCESS_SUFFIX_PATTERN =
|
|
@@ -1306,9 +1497,9 @@ function stripSuccessSuffix(text: string): string {
|
|
|
1306
1497
|
return cleaned;
|
|
1307
1498
|
}
|
|
1308
1499
|
|
|
1309
|
-
function
|
|
1500
|
+
function sanitizeProgressText(
|
|
1310
1501
|
text: string,
|
|
1311
|
-
maxChars =
|
|
1502
|
+
maxChars = MAX_SAFE_PROGRESS_CHARS,
|
|
1312
1503
|
): string {
|
|
1313
1504
|
const stripped = stripTerminalControls(text);
|
|
1314
1505
|
const withoutMarkdown = stripMarkdownFormatting(stripped);
|
|
@@ -1316,7 +1507,8 @@ function sanitizeTldrText(
|
|
|
1316
1507
|
const withoutLeaks = stripIdentifierLeaks(withoutScaffolding) || withoutScaffolding;
|
|
1317
1508
|
const withoutDangling = stripDanglingPrepositions(withoutLeaks) || withoutLeaks;
|
|
1318
1509
|
const withoutSuccess = stripSuccessSuffix(withoutDangling) || withoutDangling;
|
|
1319
|
-
|
|
1510
|
+
const rewritten = rewriteBannedFirstWord(withoutSuccess);
|
|
1511
|
+
return truncateText(rewritten, maxChars);
|
|
1320
1512
|
}
|
|
1321
1513
|
|
|
1322
1514
|
function shouldShowStatus(key: string, filter: StatusFilter): boolean {
|
|
@@ -1415,7 +1607,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1415
1607
|
let statusFilter: StatusFilter = { mode: "all", hidden: new Set() };
|
|
1416
1608
|
const seenStatusKeys = new Set<string>();
|
|
1417
1609
|
const refresh = () => requestRender?.();
|
|
1418
|
-
const
|
|
1610
|
+
const progress = new FooterProgressEngine(refresh);
|
|
1419
1611
|
const restoreStatusFilter = (ctx: ExtensionContext) => {
|
|
1420
1612
|
let restoredFilter = readGlobalStatusFilter();
|
|
1421
1613
|
if (!restoredFilter) {
|
|
@@ -1603,32 +1795,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
1603
1795
|
pi.on("thinking_level_select", async () => refresh());
|
|
1604
1796
|
pi.on("turn_end", async () => refresh());
|
|
1605
1797
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
1606
|
-
|
|
1798
|
+
progress.recordUserMessage(ctx, event.prompt);
|
|
1607
1799
|
});
|
|
1608
1800
|
pi.on("message_update", async (event, ctx) => {
|
|
1609
|
-
|
|
1801
|
+
progress.recordAssistantUpdate(ctx, event.message);
|
|
1610
1802
|
});
|
|
1611
1803
|
pi.on("tool_call", async (event, ctx) => {
|
|
1612
|
-
|
|
1804
|
+
progress.recordToolCall(ctx, event);
|
|
1613
1805
|
});
|
|
1614
1806
|
pi.on("tool_result", async (event, ctx) => {
|
|
1615
|
-
|
|
1807
|
+
progress.recordToolResult(ctx, event);
|
|
1616
1808
|
});
|
|
1617
1809
|
pi.on("message_end", async (event, ctx) => {
|
|
1618
|
-
|
|
1810
|
+
progress.recordMessageEnd(ctx, event.message);
|
|
1619
1811
|
refresh();
|
|
1620
1812
|
});
|
|
1621
1813
|
pi.on("session_before_tree", async () => {
|
|
1622
|
-
|
|
1814
|
+
progress.shutdown();
|
|
1623
1815
|
});
|
|
1624
1816
|
pi.on("session_tree", async (_event, ctx) => {
|
|
1625
|
-
|
|
1817
|
+
progress.startSession(ctx.cwd);
|
|
1626
1818
|
restoreStatusFilter(ctx);
|
|
1627
1819
|
refresh();
|
|
1628
1820
|
});
|
|
1629
1821
|
|
|
1630
1822
|
pi.on("session_start", async (_event, ctx) => {
|
|
1631
|
-
|
|
1823
|
+
progress.startSession(ctx.cwd);
|
|
1632
1824
|
restoreStatusFilter(ctx);
|
|
1633
1825
|
|
|
1634
1826
|
if (!ctx.hasUI) return;
|
|
@@ -1662,13 +1854,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1662
1854
|
const contextText = usage
|
|
1663
1855
|
? `${usage.percent !== null ? `${usage.percent.toFixed(1)}%` : "—%"} / ${formatTokens(usage.contextWindow)}`
|
|
1664
1856
|
: "—";
|
|
1665
|
-
const
|
|
1857
|
+
const progressText = progress.text();
|
|
1666
1858
|
|
|
1667
1859
|
const segmentRenderers: Record<SegmentName, string | null> = {
|
|
1668
1860
|
model: theme.fg("accent", modelName),
|
|
1669
1861
|
thinking: theme.fg(thinkingColor(thinkingLevel), `think:${thinkingLevel}`),
|
|
1670
1862
|
context: theme.fg(contextSegmentColor, contextText),
|
|
1671
|
-
|
|
1863
|
+
progress: progressText ? theme.fg("muted", progressText) : null,
|
|
1672
1864
|
extensions: extensionStatuses ? theme.fg("muted", extensionStatuses) : null,
|
|
1673
1865
|
};
|
|
1674
1866
|
|
|
@@ -1685,7 +1877,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1685
1877
|
});
|
|
1686
1878
|
|
|
1687
1879
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1688
|
-
|
|
1880
|
+
progress.shutdown();
|
|
1689
1881
|
if (ctx.hasUI) ctx.ui.setFooter(undefined);
|
|
1690
1882
|
});
|
|
1691
1883
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bar",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Pi footer/statusline showing active model, thinking level, context pressure, live
|
|
3
|
+
"version": "0.3.30",
|
|
4
|
+
"description": "Pi footer/statusline showing active model, thinking level, context pressure, live progress updates, and extension statuses.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"thinking",
|
|
15
15
|
"context",
|
|
16
16
|
"context-window",
|
|
17
|
+
"progress",
|
|
17
18
|
"tldr"
|
|
18
19
|
],
|
|
19
20
|
"license": "MIT",
|
|
@@ -51,6 +52,6 @@
|
|
|
51
52
|
"extensions": [
|
|
52
53
|
"./extensions/status-footer.ts"
|
|
53
54
|
],
|
|
54
|
-
"image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.
|
|
55
|
+
"image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.30/assets/screenshot-yellow.png"
|
|
55
56
|
}
|
|
56
57
|
}
|