pi-bar 0.3.29 → 0.3.31
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 +19 -20
- package/extensions/status-footer.ts +155 -122
- 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,
|
|
6
|
+
**Never accidentally run Opus on a typo again.** pi-bar keeps your model, thinking level, context pressure, a live progress update, and any extension statuses visible in pi's footer.
|
|
7
7
|
|
|
8
8
|
```text
|
|
9
|
-
claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯
|
|
9
|
+
claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Reviewing 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
|
|
22
|
-
- **Keep extension statuses visible** — statuses
|
|
21
|
+
- **Follow what pi is doing** — a one-line progress update keeps the current task visible without scrolling.
|
|
22
|
+
- **Keep extension statuses visible** — statuses other extensions set still appear in the 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,36 +42,35 @@ 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 stays hidden until pi-bar has a current update. The `extensions` segment stays hidden when no extension has set a status.
|
|
50
50
|
|
|
51
|
-
### Configure live
|
|
51
|
+
### Configure live progress updates
|
|
52
52
|
|
|
53
|
-
pi-bar shows a
|
|
53
|
+
pi-bar shows a short, plain-English description of what pi is working on right now. It refreshes as pi works and resets when you switch branches in the session tree, so stale updates never follow you across tasks. Set `PI_BAR_SHOW` without `progress` to hide it.
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Pick a specific model for the update by setting the env var or pi settings:
|
|
56
56
|
|
|
57
57
|
```bash
|
|
58
|
-
|
|
58
|
+
PI_BAR_PROGRESS_MODEL=anthropic/claude-haiku-4-5 pi
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
Or in pi settings: `bar.progressModel`. Otherwise pi-bar picks a fast model you are already authenticated with.
|
|
62
62
|
|
|
63
63
|
### Configure extension statuses
|
|
64
64
|
|
|
65
|
-
pi-bar
|
|
66
|
-
Use `/bar` inside pi to open an interactive status-visibility picker.
|
|
65
|
+
Other pi extensions can publish small status badges. Pi-bar collects them into the `extensions` segment. Run `/bar` inside pi to pick which ones to show:
|
|
67
66
|
|
|
68
67
|
```text
|
|
69
68
|
/bar
|
|
70
69
|
```
|
|
71
70
|
|
|
72
|
-
Toggle each status
|
|
71
|
+
Toggle each status between `shown` and `hidden`. The `New statuses` row controls the default for badges that appear later.
|
|
73
72
|
|
|
74
|
-
|
|
73
|
+
Your choices persist across pi sessions in `~/.pi/agent/pi-bar.json`. Override the path with `PI_BAR_CONFIG=/some/path.json`.
|
|
75
74
|
|
|
76
75
|
### Change context thresholds
|
|
77
76
|
|
|
@@ -90,4 +89,4 @@ The first number is the warning/yellow threshold. The second number is the dange
|
|
|
90
89
|
|
|
91
90
|
Pi extensions run with your local user permissions. Review any pi package source before installing it.
|
|
92
91
|
|
|
93
|
-
|
|
92
|
+
Progress updates send short snippets of your session activity to the selected model provider. Disable the `progress` segment with `PI_BAR_SHOW` if that is not acceptable.
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pi-bar — footer/statusline extension.
|
|
2
|
+
* pi-bar — footer / statusline extension.
|
|
3
3
|
*
|
|
4
|
-
* Replaces
|
|
5
|
-
* <model
|
|
4
|
+
* Replaces pi's built-in footer with left-aligned segments:
|
|
5
|
+
* <model> ❯ think:<level> ❯ <context% / window> ❯ <progress> ❯ <extensions>
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
* claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M
|
|
7
|
+
* Example:
|
|
8
|
+
* claude-opus-4.7 ❯ think:med ❯ 2.6% / 1.0M ❯ Reviewing package structure
|
|
9
9
|
*
|
|
10
|
-
* Re-renders on model change, thinking-level change, status updates,
|
|
11
|
-
*
|
|
10
|
+
* Re-renders on model change, thinking-level change, status updates, and after
|
|
11
|
+
* each assistant turn so context usage stays current.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
* PI_BAR_SHOW
|
|
15
|
-
* PI_BAR_THRESHOLDS
|
|
13
|
+
* Environment variables:
|
|
14
|
+
* PI_BAR_SHOW comma-separated list of segments to show
|
|
15
|
+
* PI_BAR_THRESHOLDS warning,danger context-usage percentages
|
|
16
|
+
* PI_BAR_PROGRESS_MODEL provider/id for the progress update model
|
|
17
|
+
* PI_BAR_CONFIG override the persisted status-filter config path
|
|
16
18
|
*/
|
|
17
19
|
|
|
18
20
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
@@ -33,39 +35,39 @@ import {
|
|
|
33
35
|
truncateToWidth,
|
|
34
36
|
} from "@earendil-works/pi-tui";
|
|
35
37
|
|
|
36
|
-
type SegmentName = "model" | "thinking" | "context" | "
|
|
38
|
+
type SegmentName = "model" | "thinking" | "context" | "progress" | "extensions";
|
|
37
39
|
type StatusFilter =
|
|
38
40
|
| { mode: "all"; hidden: Set<string> }
|
|
39
41
|
| { mode: "only"; shown: Set<string> };
|
|
40
42
|
type SerializedStatusFilter =
|
|
41
43
|
| { mode: "all"; hidden: string[] }
|
|
42
44
|
| { mode: "only"; shown: string[] };
|
|
43
|
-
type
|
|
45
|
+
type ProgressActivityType =
|
|
44
46
|
| "user_message"
|
|
45
47
|
| "assistant_update"
|
|
46
48
|
| "tool_call"
|
|
47
49
|
| "tool_result"
|
|
48
50
|
| "assistant_final"
|
|
49
51
|
| "assistant_failure";
|
|
50
|
-
type
|
|
51
|
-
type
|
|
52
|
+
type ProgressDisplayPriority = "immediate" | "normal" | "final";
|
|
53
|
+
type ProgressActivity = {
|
|
52
54
|
index: number;
|
|
53
|
-
activityType:
|
|
54
|
-
displayPriority:
|
|
55
|
+
activityType: ProgressActivityType;
|
|
56
|
+
displayPriority: ProgressDisplayPriority;
|
|
55
57
|
text: string;
|
|
56
58
|
toolCallId?: string;
|
|
57
59
|
};
|
|
58
|
-
type
|
|
60
|
+
type ProgressCheckpoint = {
|
|
59
61
|
activityIndex: number;
|
|
60
|
-
displayPriority:
|
|
62
|
+
displayPriority: ProgressDisplayPriority;
|
|
61
63
|
text: string;
|
|
62
64
|
};
|
|
63
|
-
type
|
|
65
|
+
type ProgressCheckpointJob = {
|
|
64
66
|
activityIndex: number;
|
|
65
|
-
displayPriority:
|
|
67
|
+
displayPriority: ProgressDisplayPriority;
|
|
66
68
|
runId: number;
|
|
67
69
|
};
|
|
68
|
-
type
|
|
70
|
+
type ProgressModelPreference = { provider: string; id: string };
|
|
69
71
|
type FastModelAuth = {
|
|
70
72
|
model: Parameters<typeof complete>[0];
|
|
71
73
|
apiKey: string;
|
|
@@ -73,7 +75,10 @@ type FastModelAuth = {
|
|
|
73
75
|
};
|
|
74
76
|
|
|
75
77
|
const STATUS_FILTER_ENTRY_TYPE = "pi-bar-status-filter";
|
|
76
|
-
const
|
|
78
|
+
const SETTINGS_PROGRESS_KEY = "progress";
|
|
79
|
+
// Legacy settings/env names. The `tldr` keys remain accepted so existing user
|
|
80
|
+
// configs continue to work after the rename to "progress updates".
|
|
81
|
+
const LEGACY_SETTINGS_TLDR_KEY = "tldr";
|
|
77
82
|
const SETTINGS_BAR_KEY = "bar";
|
|
78
83
|
const MAX_ACTIVITY_TEXT_CHARS = 800;
|
|
79
84
|
const MAX_USER_TEXT_CHARS = 700;
|
|
@@ -84,10 +89,10 @@ const MAX_FINAL_TEXT_CHARS = 700;
|
|
|
84
89
|
const MAX_RETAINED_RAW_ACTIVITIES = 128;
|
|
85
90
|
const ANSI_PATTERN = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
86
91
|
const MAX_CONTEXT_CHECKPOINTS = 8;
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
const
|
|
92
|
+
const PROGRESS_MAX_TOKENS = 120;
|
|
93
|
+
const PROGRESS_REQUEST_TIMEOUT_MS = 2_000;
|
|
94
|
+
const PROGRESS_DISPLAY_UPDATE_INTERVAL_MS = 1_200;
|
|
95
|
+
const PROGRESS_TARGET_SUMMARY_CHARS = 60;
|
|
91
96
|
// Burst debouncing on the generation side: coalesce rapid-fire activities into
|
|
92
97
|
// one model call. Quiet window catches short bursts; max wait surfaces progress
|
|
93
98
|
// during continuous activity.
|
|
@@ -97,7 +102,7 @@ const NORMAL_CHECKPOINT_QUIET_MS = 1_500;
|
|
|
97
102
|
const NORMAL_CHECKPOINT_MAX_WAIT_MS = 2_500;
|
|
98
103
|
// Defensive ceiling: even if the model ignores the length instruction, never
|
|
99
104
|
// blast a large payload into the footer.
|
|
100
|
-
const
|
|
105
|
+
const MAX_SAFE_PROGRESS_CHARS = 240;
|
|
101
106
|
const CONFIG_PATH =
|
|
102
107
|
process.env.PI_BAR_CONFIG ?? join(homedir(), ".pi", "agent", "pi-bar.json");
|
|
103
108
|
|
|
@@ -105,7 +110,7 @@ const DEFAULT_SEGMENTS: SegmentName[] = [
|
|
|
105
110
|
"model",
|
|
106
111
|
"thinking",
|
|
107
112
|
"context",
|
|
108
|
-
"
|
|
113
|
+
"progress",
|
|
109
114
|
"extensions",
|
|
110
115
|
];
|
|
111
116
|
const DEFAULT_WARNING_THRESHOLD = 70;
|
|
@@ -168,11 +173,14 @@ function parseSegments(): SegmentName[] {
|
|
|
168
173
|
const raw = process.env.PI_BAR_SHOW;
|
|
169
174
|
if (!raw) return DEFAULT_SEGMENTS;
|
|
170
175
|
|
|
176
|
+
// `tldr` is the legacy segment name, kept working as an alias so existing
|
|
177
|
+
// PI_BAR_SHOW configs do not break after the rename to "progress".
|
|
171
178
|
const requested = raw
|
|
172
179
|
.split(",")
|
|
173
180
|
.map((segment) => segment.trim().toLowerCase())
|
|
181
|
+
.map((segment) => (segment === "tldr" ? "progress" : segment))
|
|
174
182
|
.filter((segment): segment is SegmentName =>
|
|
175
|
-
["model", "thinking", "context", "
|
|
183
|
+
["model", "thinking", "context", "progress", "extensions"].includes(segment),
|
|
176
184
|
);
|
|
177
185
|
|
|
178
186
|
return requested.length > 0 ? requested : DEFAULT_SEGMENTS;
|
|
@@ -206,7 +214,7 @@ function parseThresholds(): { warningThreshold: number; errorThreshold: number }
|
|
|
206
214
|
};
|
|
207
215
|
}
|
|
208
216
|
|
|
209
|
-
function
|
|
217
|
+
function parseProgressModelSpec(value: string): ProgressModelPreference | undefined {
|
|
210
218
|
const trimmed = value.trim();
|
|
211
219
|
if (!trimmed || trimmed === "auto") return undefined;
|
|
212
220
|
const separator = trimmed.indexOf("/");
|
|
@@ -217,51 +225,64 @@ function parseTldrModelSpec(value: string): TldrModelPreference | undefined {
|
|
|
217
225
|
function settingsModelValue(settings: Record<string, unknown>): string | undefined {
|
|
218
226
|
const bar = settings[SETTINGS_BAR_KEY];
|
|
219
227
|
if (bar && typeof bar === "object" && !Array.isArray(bar)) {
|
|
220
|
-
const
|
|
228
|
+
const record = bar as Record<string, unknown>;
|
|
229
|
+
// Canonical key after rename. Falls through to legacy `tldrModel` so old
|
|
230
|
+
// settings still work without manual migration.
|
|
231
|
+
if (typeof record.progressModel === "string") return record.progressModel;
|
|
232
|
+
if (typeof record.tldrModel === "string") return record.tldrModel;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const progressSection = settings[SETTINGS_PROGRESS_KEY];
|
|
236
|
+
if (progressSection && typeof progressSection === "object" && !Array.isArray(progressSection)) {
|
|
237
|
+
const value = (progressSection as Record<string, unknown>).model;
|
|
221
238
|
if (typeof value === "string") return value;
|
|
222
239
|
}
|
|
223
240
|
|
|
224
|
-
const
|
|
225
|
-
if (
|
|
226
|
-
const value = (
|
|
241
|
+
const legacy = settings[LEGACY_SETTINGS_TLDR_KEY];
|
|
242
|
+
if (legacy && typeof legacy === "object" && !Array.isArray(legacy)) {
|
|
243
|
+
const value = (legacy as Record<string, unknown>).model;
|
|
227
244
|
if (typeof value === "string") return value;
|
|
228
245
|
}
|
|
229
246
|
|
|
230
247
|
return undefined;
|
|
231
248
|
}
|
|
232
249
|
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
250
|
+
function resolveProgressModelPreference(cwd: string): ProgressModelPreference | undefined {
|
|
251
|
+
// PI_BAR_PROGRESS_MODEL is the canonical env var. PI_BAR_TLDR_MODEL is
|
|
252
|
+
// honored as a backwards-compat alias for users who set it before the
|
|
253
|
+
// rename.
|
|
254
|
+
const envModel = process.env.PI_BAR_PROGRESS_MODEL ?? process.env.PI_BAR_TLDR_MODEL;
|
|
255
|
+
if (envModel) return parseProgressModelSpec(envModel);
|
|
236
256
|
|
|
237
257
|
const settings = SettingsManager.create(cwd);
|
|
238
258
|
const projectModel = settingsModelValue(
|
|
239
259
|
settings.getProjectSettings() as Record<string, unknown>,
|
|
240
260
|
);
|
|
241
|
-
if (projectModel !== undefined) return
|
|
261
|
+
if (projectModel !== undefined) return parseProgressModelSpec(projectModel);
|
|
242
262
|
|
|
243
263
|
const globalModel = settingsModelValue(
|
|
244
264
|
settings.getGlobalSettings() as Record<string, unknown>,
|
|
245
265
|
);
|
|
246
|
-
return globalModel ?
|
|
266
|
+
return globalModel ? parseProgressModelSpec(globalModel) : undefined;
|
|
247
267
|
}
|
|
248
268
|
|
|
249
|
-
// Fallback order optimizes for low latency + low cost first. The
|
|
250
|
-
// not need reasoning quality; codex/gpt-5.4-mini is fastest
|
|
251
|
-
|
|
269
|
+
// Fallback order optimizes for low latency + low cost first. The progress
|
|
270
|
+
// update path does not need reasoning quality; codex/gpt-5.4-mini is fastest
|
|
271
|
+
// in practice.
|
|
272
|
+
const FAST_PROGRESS_MODELS: readonly ProgressModelPreference[] = [
|
|
252
273
|
{ provider: "openai-codex", id: "gpt-5.4-mini" },
|
|
253
274
|
{ provider: "openai-codex", id: "gpt-5.3-codex-spark" },
|
|
254
275
|
{ provider: "anthropic", id: "claude-haiku-4-5" },
|
|
255
276
|
{ provider: "anthropic", id: "claude-haiku-4-5-20251001" },
|
|
256
277
|
];
|
|
257
278
|
|
|
258
|
-
function
|
|
279
|
+
function formatProgressModelKey(model: ProgressModelPreference): string {
|
|
259
280
|
return `${model.provider}/${model.id}`;
|
|
260
281
|
}
|
|
261
282
|
|
|
262
283
|
async function getModelAuth(
|
|
263
284
|
ctx: ExtensionContext,
|
|
264
|
-
preference:
|
|
285
|
+
preference: ProgressModelPreference,
|
|
265
286
|
): Promise<FastModelAuth | undefined> {
|
|
266
287
|
const model = ctx.modelRegistry.find(preference.provider, preference.id);
|
|
267
288
|
if (!model) return undefined;
|
|
@@ -272,17 +293,17 @@ async function getModelAuth(
|
|
|
272
293
|
: undefined;
|
|
273
294
|
}
|
|
274
295
|
|
|
275
|
-
async function
|
|
296
|
+
async function getFastProgressModelAuth(
|
|
276
297
|
ctx: ExtensionContext,
|
|
277
|
-
configuredModel?:
|
|
298
|
+
configuredModel?: ProgressModelPreference,
|
|
278
299
|
): Promise<FastModelAuth | undefined> {
|
|
279
300
|
if (configuredModel) {
|
|
280
301
|
const configuredAuth = await getModelAuth(ctx, configuredModel);
|
|
281
302
|
if (configuredAuth) return configuredAuth;
|
|
282
303
|
}
|
|
283
304
|
|
|
284
|
-
for (const candidate of
|
|
285
|
-
if (configuredModel &&
|
|
305
|
+
for (const candidate of FAST_PROGRESS_MODELS) {
|
|
306
|
+
if (configuredModel && formatProgressModelKey(candidate) === formatProgressModelKey(configuredModel)) {
|
|
286
307
|
continue;
|
|
287
308
|
}
|
|
288
309
|
const auth = await getModelAuth(ctx, candidate);
|
|
@@ -450,16 +471,16 @@ function extractTextContent(content: readonly unknown[] | undefined): string | u
|
|
|
450
471
|
return compacted.length > 0 ? compacted : undefined;
|
|
451
472
|
}
|
|
452
473
|
|
|
453
|
-
class
|
|
474
|
+
class ProgressFactCollector {
|
|
454
475
|
private nextIndex = 1;
|
|
455
|
-
private readonly activities:
|
|
476
|
+
private readonly activities: ProgressActivity[] = [];
|
|
456
477
|
|
|
457
478
|
resetConversation(): void {
|
|
458
479
|
this.nextIndex = 1;
|
|
459
480
|
this.activities.splice(0);
|
|
460
481
|
}
|
|
461
482
|
|
|
462
|
-
recordUserMessage(prompt: string):
|
|
483
|
+
recordUserMessage(prompt: string): ProgressActivity {
|
|
463
484
|
return this.addActivity(
|
|
464
485
|
"user_message",
|
|
465
486
|
"immediate",
|
|
@@ -467,7 +488,7 @@ class TldrFactCollector {
|
|
|
467
488
|
);
|
|
468
489
|
}
|
|
469
490
|
|
|
470
|
-
recordAssistantUpdate(message: unknown):
|
|
491
|
+
recordAssistantUpdate(message: unknown): ProgressActivity | undefined {
|
|
471
492
|
if (!message || typeof message !== "object") return undefined;
|
|
472
493
|
const record = message as Record<string, unknown>;
|
|
473
494
|
if (record.role !== "assistant") return undefined;
|
|
@@ -492,7 +513,7 @@ class TldrFactCollector {
|
|
|
492
513
|
toolName: string;
|
|
493
514
|
input?: Record<string, unknown>;
|
|
494
515
|
toolCallId?: string;
|
|
495
|
-
}):
|
|
516
|
+
}): ProgressActivity {
|
|
496
517
|
return this.addActivity(
|
|
497
518
|
"tool_call",
|
|
498
519
|
"normal",
|
|
@@ -508,7 +529,7 @@ class TldrFactCollector {
|
|
|
508
529
|
content?: readonly unknown[];
|
|
509
530
|
details?: unknown;
|
|
510
531
|
toolCallId?: string;
|
|
511
|
-
}):
|
|
532
|
+
}): ProgressActivity {
|
|
512
533
|
const isError = Boolean(event.isError);
|
|
513
534
|
const resultText = summarizeToolResult(
|
|
514
535
|
event.toolName,
|
|
@@ -540,7 +561,7 @@ class TldrFactCollector {
|
|
|
540
561
|
);
|
|
541
562
|
}
|
|
542
563
|
|
|
543
|
-
recordMessageEnd(message: unknown):
|
|
564
|
+
recordMessageEnd(message: unknown): ProgressActivity | "emptyFinalStop" | "ignored" {
|
|
544
565
|
if (!message || typeof message !== "object") return "ignored";
|
|
545
566
|
const record = message as Record<string, unknown>;
|
|
546
567
|
if (record.role !== "assistant") return "ignored";
|
|
@@ -569,7 +590,7 @@ class TldrFactCollector {
|
|
|
569
590
|
: "ignored";
|
|
570
591
|
}
|
|
571
592
|
|
|
572
|
-
activitiesAfter(previousIndex: number, throughIndex: number): readonly
|
|
593
|
+
activitiesAfter(previousIndex: number, throughIndex: number): readonly ProgressActivity[] {
|
|
573
594
|
return this.activities.filter(
|
|
574
595
|
(activity) => activity.index > previousIndex && activity.index <= throughIndex,
|
|
575
596
|
);
|
|
@@ -591,11 +612,11 @@ class TldrFactCollector {
|
|
|
591
612
|
}
|
|
592
613
|
|
|
593
614
|
private addActivity(
|
|
594
|
-
activityType:
|
|
595
|
-
displayPriority:
|
|
615
|
+
activityType: ProgressActivityType,
|
|
616
|
+
displayPriority: ProgressDisplayPriority,
|
|
596
617
|
text: string,
|
|
597
618
|
toolCallId?: string,
|
|
598
|
-
):
|
|
619
|
+
): ProgressActivity {
|
|
599
620
|
const activity = {
|
|
600
621
|
index: this.nextIndex,
|
|
601
622
|
activityType,
|
|
@@ -612,25 +633,25 @@ class TldrFactCollector {
|
|
|
612
633
|
}
|
|
613
634
|
}
|
|
614
635
|
|
|
615
|
-
class
|
|
616
|
-
private readonly facts = new
|
|
617
|
-
private configuredModel?:
|
|
636
|
+
class FooterProgressEngine {
|
|
637
|
+
private readonly facts = new ProgressFactCollector();
|
|
638
|
+
private configuredModel?: ProgressModelPreference;
|
|
618
639
|
private runId = 0;
|
|
619
640
|
private latestAcceptedActivityIndex = 0;
|
|
620
641
|
private lastRenderedActivityIndex = 0;
|
|
621
642
|
private lastRenderedText = "";
|
|
622
643
|
private lastDisplayAt = Number.NEGATIVE_INFINITY;
|
|
623
|
-
private readonly acceptedCheckpoints:
|
|
624
|
-
private checkpointQueue:
|
|
625
|
-
private inFlightCheckpoint?:
|
|
626
|
-
private pendingDisplayCheckpoint?:
|
|
644
|
+
private readonly acceptedCheckpoints: ProgressCheckpoint[] = [];
|
|
645
|
+
private checkpointQueue: ProgressCheckpointJob[] = [];
|
|
646
|
+
private inFlightCheckpoint?: ProgressCheckpointJob;
|
|
647
|
+
private pendingDisplayCheckpoint?: ProgressCheckpoint;
|
|
627
648
|
private displayTimer?: ReturnType<typeof setTimeout>;
|
|
628
649
|
private abortController?: AbortController;
|
|
629
650
|
private currentText: string | null = null;
|
|
630
651
|
// Normal-priority bursts wait for a quiet window before triggering a model
|
|
631
652
|
// call. Pi often emits many rapid activities (e.g. streaming reads); without
|
|
632
|
-
// debouncing we would hammer the
|
|
633
|
-
private pendingNormalCheckpoint?:
|
|
653
|
+
// debouncing we would hammer the progress update model for intermediate states.
|
|
654
|
+
private pendingNormalCheckpoint?: ProgressCheckpointJob;
|
|
634
655
|
private normalCheckpointTimer?: ReturnType<typeof setTimeout>;
|
|
635
656
|
private normalCheckpointBurstStartedAt?: number;
|
|
636
657
|
|
|
@@ -641,7 +662,7 @@ class FooterTldrEngine {
|
|
|
641
662
|
}
|
|
642
663
|
|
|
643
664
|
startSession(cwd: string): void {
|
|
644
|
-
this.configuredModel =
|
|
665
|
+
this.configuredModel = resolveProgressModelPreference(cwd);
|
|
645
666
|
this.facts.resetConversation();
|
|
646
667
|
this.startFreshRun();
|
|
647
668
|
}
|
|
@@ -652,7 +673,7 @@ class FooterTldrEngine {
|
|
|
652
673
|
}
|
|
653
674
|
|
|
654
675
|
recordUserMessage(ctx: ExtensionContext, prompt: string): void {
|
|
655
|
-
// A new user turn replaces the entire visible
|
|
676
|
+
// A new user turn replaces the entire visible progress update context, so wipe both
|
|
656
677
|
// the rendered text and the prior-turn checkpoint context that feeds the
|
|
657
678
|
// model. Carrying old checkpoints biased the next turn's phrasing toward
|
|
658
679
|
// the previous task.
|
|
@@ -766,7 +787,7 @@ class FooterTldrEngine {
|
|
|
766
787
|
|
|
767
788
|
private scheduleNormalCheckpoint(
|
|
768
789
|
ctx: ExtensionContext,
|
|
769
|
-
job:
|
|
790
|
+
job: ProgressCheckpointJob,
|
|
770
791
|
): void {
|
|
771
792
|
this.pendingNormalCheckpoint = job;
|
|
772
793
|
this.normalCheckpointBurstStartedAt ??= performance.now();
|
|
@@ -804,7 +825,7 @@ class FooterTldrEngine {
|
|
|
804
825
|
this.pump(ctx);
|
|
805
826
|
}
|
|
806
827
|
|
|
807
|
-
private enqueue(ctx: ExtensionContext, activity:
|
|
828
|
+
private enqueue(ctx: ExtensionContext, activity: ProgressActivity): void {
|
|
808
829
|
if (!ctx.hasUI) return;
|
|
809
830
|
const job = {
|
|
810
831
|
activityIndex: activity.index,
|
|
@@ -868,19 +889,19 @@ class FooterTldrEngine {
|
|
|
868
889
|
void this.runCheckpointRequest(ctx, job);
|
|
869
890
|
}
|
|
870
891
|
|
|
871
|
-
private isCurrentJob(job:
|
|
892
|
+
private isCurrentJob(job: ProgressCheckpointJob): boolean {
|
|
872
893
|
return job.runId === this.runId && this.inFlightCheckpoint === job;
|
|
873
894
|
}
|
|
874
895
|
|
|
875
896
|
private async runCheckpointRequest(
|
|
876
897
|
ctx: ExtensionContext,
|
|
877
|
-
job:
|
|
898
|
+
job: ProgressCheckpointJob,
|
|
878
899
|
): Promise<void> {
|
|
879
900
|
let abortController: AbortController | undefined;
|
|
880
901
|
try {
|
|
881
902
|
const prompt = this.checkpointPrompt(job);
|
|
882
903
|
if (!prompt) return;
|
|
883
|
-
const auth = await
|
|
904
|
+
const auth = await getFastProgressModelAuth(ctx, this.configuredModel);
|
|
884
905
|
if (!this.isCurrentJob(job) || !auth) return;
|
|
885
906
|
|
|
886
907
|
abortController = new AbortController();
|
|
@@ -891,17 +912,17 @@ class FooterTldrEngine {
|
|
|
891
912
|
{
|
|
892
913
|
apiKey: auth.apiKey,
|
|
893
914
|
headers: auth.headers,
|
|
894
|
-
maxTokens:
|
|
915
|
+
maxTokens: PROGRESS_MAX_TOKENS,
|
|
895
916
|
maxRetries: 0,
|
|
896
917
|
cacheRetention: "none",
|
|
897
|
-
timeoutMs:
|
|
918
|
+
timeoutMs: PROGRESS_REQUEST_TIMEOUT_MS,
|
|
898
919
|
signal: abortController.signal,
|
|
899
920
|
},
|
|
900
921
|
);
|
|
901
922
|
if (!this.isCurrentJob(job) || response.stopReason !== "stop") return;
|
|
902
923
|
|
|
903
924
|
const rawText = extractTextContent(response.content) ?? "";
|
|
904
|
-
const text =
|
|
925
|
+
const text = sanitizeProgressText(rawText);
|
|
905
926
|
if (!text) return;
|
|
906
927
|
const checkpoint = {
|
|
907
928
|
activityIndex: job.activityIndex,
|
|
@@ -921,7 +942,7 @@ class FooterTldrEngine {
|
|
|
921
942
|
}
|
|
922
943
|
}
|
|
923
944
|
|
|
924
|
-
private acceptCheckpoint(checkpoint:
|
|
945
|
+
private acceptCheckpoint(checkpoint: ProgressCheckpoint): void {
|
|
925
946
|
this.acceptedCheckpoints.push(checkpoint);
|
|
926
947
|
if (this.acceptedCheckpoints.length > MAX_CONTEXT_CHECKPOINTS) {
|
|
927
948
|
this.acceptedCheckpoints.splice(
|
|
@@ -935,7 +956,7 @@ class FooterTldrEngine {
|
|
|
935
956
|
|
|
936
957
|
private considerDisplayingCheckpoint(
|
|
937
958
|
ctx: ExtensionContext,
|
|
938
|
-
checkpoint:
|
|
959
|
+
checkpoint: ProgressCheckpoint,
|
|
939
960
|
): void {
|
|
940
961
|
if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
|
|
941
962
|
if (checkpoint.activityIndex !== this.facts.latestActivityIndex()) return;
|
|
@@ -947,7 +968,7 @@ class FooterTldrEngine {
|
|
|
947
968
|
}
|
|
948
969
|
|
|
949
970
|
const elapsedMs = performance.now() - this.lastDisplayAt;
|
|
950
|
-
if (elapsedMs >=
|
|
971
|
+
if (elapsedMs >= PROGRESS_DISPLAY_UPDATE_INTERVAL_MS) {
|
|
951
972
|
this.clearPendingDisplay();
|
|
952
973
|
this.renderCheckpoint(checkpoint);
|
|
953
974
|
return;
|
|
@@ -961,16 +982,16 @@ class FooterTldrEngine {
|
|
|
961
982
|
this.pendingDisplayCheckpoint = undefined;
|
|
962
983
|
if (!pending || pending.activityIndex !== this.facts.latestActivityIndex()) return;
|
|
963
984
|
this.renderCheckpoint(pending);
|
|
964
|
-
},
|
|
985
|
+
}, PROGRESS_DISPLAY_UPDATE_INTERVAL_MS - elapsedMs);
|
|
965
986
|
}
|
|
966
987
|
|
|
967
|
-
private renderCheckpoint(checkpoint:
|
|
988
|
+
private renderCheckpoint(checkpoint: ProgressCheckpoint): void {
|
|
968
989
|
if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
|
|
969
990
|
if (checkpoint.text === this.lastRenderedText) return;
|
|
970
991
|
// Skip near-duplicates: a burst of consecutive checkpoints often produces
|
|
971
992
|
// the same action fragment with only punctuation/casing differences. They
|
|
972
993
|
// flash visibly in the footer without adding information.
|
|
973
|
-
if (
|
|
994
|
+
if (isNearDuplicateProgress(checkpoint.text, this.lastRenderedText)) {
|
|
974
995
|
this.lastRenderedActivityIndex = checkpoint.activityIndex;
|
|
975
996
|
return;
|
|
976
997
|
}
|
|
@@ -981,7 +1002,7 @@ class FooterTldrEngine {
|
|
|
981
1002
|
this.requestRender();
|
|
982
1003
|
}
|
|
983
1004
|
|
|
984
|
-
private checkpointPrompt(job:
|
|
1005
|
+
private checkpointPrompt(job: ProgressCheckpointJob): UserMessage | undefined {
|
|
985
1006
|
const rawActivities = this.facts.activitiesAfter(
|
|
986
1007
|
this.latestAcceptedActivityIndex,
|
|
987
1008
|
job.activityIndex,
|
|
@@ -993,13 +1014,13 @@ class FooterTldrEngine {
|
|
|
993
1014
|
{
|
|
994
1015
|
type: "text",
|
|
995
1016
|
text: [
|
|
996
|
-
"Prior
|
|
1017
|
+
"Prior progress updates (context only, do not copy phrasing):",
|
|
997
1018
|
previousCheckpointLines(this.acceptedCheckpoints),
|
|
998
1019
|
"",
|
|
999
1020
|
"New activity to summarize:",
|
|
1000
1021
|
...rawActivities.map(formatRawActivity),
|
|
1001
1022
|
"",
|
|
1002
|
-
"Write the next
|
|
1023
|
+
"Write the next progress update.",
|
|
1003
1024
|
].join("\n"),
|
|
1004
1025
|
},
|
|
1005
1026
|
],
|
|
@@ -1090,11 +1111,12 @@ const BANNED_FIRST_WORDS = [
|
|
|
1090
1111
|
"Find",
|
|
1091
1112
|
] as const;
|
|
1092
1113
|
|
|
1093
|
-
function checkpointSystemPrompt(job:
|
|
1094
|
-
// Tense + exemplars vary by priority. Final
|
|
1095
|
-
// using -ing examples for final caused all
|
|
1096
|
-
// present-progressive even though the literal instruction said
|
|
1097
|
-
//
|
|
1114
|
+
function checkpointSystemPrompt(job: ProgressCheckpointJob): string {
|
|
1115
|
+
// Tense + exemplars vary by priority. Final progress updates describe
|
|
1116
|
+
// completed turns; using -ing examples for final caused all of them to slip
|
|
1117
|
+
// back into present-progressive even though the literal instruction said
|
|
1118
|
+
// past tense.
|
|
1119
|
+
// Immediate progress updates cover a brand-new user request; they need a rephrase
|
|
1098
1120
|
// directive instead of generic filler like "Continuing with task".
|
|
1099
1121
|
let tenseInstruction: string;
|
|
1100
1122
|
let goodExamples: string;
|
|
@@ -1103,7 +1125,7 @@ function checkpointSystemPrompt(job: TldrCheckpointJob): string {
|
|
|
1103
1125
|
tenseInstruction = "Start with a past-tense verb describing what was completed.";
|
|
1104
1126
|
goodExamples = [
|
|
1105
1127
|
"- Updated footer summary behavior",
|
|
1106
|
-
"- Investigated live
|
|
1128
|
+
"- Investigated live progress regressions",
|
|
1107
1129
|
"- Refined sanitizer for stray prefixes",
|
|
1108
1130
|
"- Wrapped up extension release",
|
|
1109
1131
|
].join("\n");
|
|
@@ -1113,7 +1135,7 @@ function checkpointSystemPrompt(job: TldrCheckpointJob): string {
|
|
|
1113
1135
|
"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.";
|
|
1114
1136
|
goodExamples = [
|
|
1115
1137
|
"- Reviewing footer summary behavior",
|
|
1116
|
-
"- Investigating live
|
|
1138
|
+
"- Investigating live progress regressions",
|
|
1117
1139
|
"- Refining sanitizer for stray prefixes",
|
|
1118
1140
|
"- Preparing extension release",
|
|
1119
1141
|
"- Resuming refactor work",
|
|
@@ -1123,7 +1145,7 @@ function checkpointSystemPrompt(job: TldrCheckpointJob): string {
|
|
|
1123
1145
|
tenseInstruction = "Start with a present-tense -ing verb describing current work.";
|
|
1124
1146
|
goodExamples = [
|
|
1125
1147
|
"- Reviewing footer summary behavior",
|
|
1126
|
-
"- Investigating live
|
|
1148
|
+
"- Investigating live progress regressions",
|
|
1127
1149
|
"- Refining sanitizer for stray prefixes",
|
|
1128
1150
|
"- Wrapping up extension release",
|
|
1129
1151
|
].join("\n");
|
|
@@ -1136,7 +1158,7 @@ function checkpointSystemPrompt(job: TldrCheckpointJob): string {
|
|
|
1136
1158
|
// suffixes, file path leaks, and markdown formatting. The HARD CONSTRAINTS
|
|
1137
1159
|
// block sits at the tail of the prompt because instruction-following models
|
|
1138
1160
|
// give the most weight to the most-recent directives (recency bias).
|
|
1139
|
-
return `Write one plain-English
|
|
1161
|
+
return `Write one plain-English progress update for a Pi coding agent.
|
|
1140
1162
|
Describe the work progress as if a human developer were doing it.
|
|
1141
1163
|
Focus on the task activity and current outcome, not agent mechanics.
|
|
1142
1164
|
Do not mention tools, tool calls, prompts, messages, model output, or implementation details.
|
|
@@ -1145,11 +1167,11 @@ Do not use file paths, file extensions, code identifiers, package names, or vers
|
|
|
1145
1167
|
Do not use backticks, asterisks, underscores, quotes, or any markdown formatting.
|
|
1146
1168
|
Do not append filler suffixes such as "with success", "successfully", or "completed successfully".
|
|
1147
1169
|
Do not claim progress or completion that is not present in the activity.
|
|
1148
|
-
Use the prior
|
|
1170
|
+
Use the prior progress updates for context and the new activity for the update.
|
|
1149
1171
|
Summarize the current state of work; do not narrate the history.
|
|
1150
1172
|
If context is sparse, still summarize the available activity.
|
|
1151
1173
|
Never ask for more information or say there is not enough context.
|
|
1152
|
-
Return one concise status fragment under ${
|
|
1174
|
+
Return one concise status fragment under ${PROGRESS_TARGET_SUMMARY_CHARS} characters.
|
|
1153
1175
|
Omit subjects like "the agent" or "it".
|
|
1154
1176
|
Prefer verb + direct object. Include outcome only if important.
|
|
1155
1177
|
Do not address the user.
|
|
@@ -1163,7 +1185,7 @@ Bad examples:
|
|
|
1163
1185
|
- Editing extensions/status-footer.ts with success.
|
|
1164
1186
|
- Reading status-footer file completed successfully.
|
|
1165
1187
|
- Publishing \`pi-bar@0.3.3\` to npm.
|
|
1166
|
-
- Grepping for
|
|
1188
|
+
- Grepping for sanitizeProgressText callers.
|
|
1167
1189
|
- Verifying repository status after commit.
|
|
1168
1190
|
- Investigating user input responses.
|
|
1169
1191
|
|
|
@@ -1180,7 +1202,7 @@ function extractStopReason(message: unknown): string | undefined {
|
|
|
1180
1202
|
return typeof stopReason === "string" ? stopReason : undefined;
|
|
1181
1203
|
}
|
|
1182
1204
|
|
|
1183
|
-
// Two
|
|
1205
|
+
// Two progress updates are near-duplicates if their normalized action fragments (lowercase,
|
|
1184
1206
|
// punctuation/whitespace collapsed) match. Catches bursts of cosmetic variants
|
|
1185
1207
|
// like "Editing X." vs "Editing X" vs "editing x.".
|
|
1186
1208
|
function normalizedActionFragment(text: string): string {
|
|
@@ -1191,25 +1213,25 @@ function normalizedActionFragment(text: string): string {
|
|
|
1191
1213
|
.trim();
|
|
1192
1214
|
}
|
|
1193
1215
|
|
|
1194
|
-
function
|
|
1216
|
+
function isNearDuplicateProgress(current: string, previous: string): boolean {
|
|
1195
1217
|
if (!previous) return false;
|
|
1196
1218
|
return normalizedActionFragment(current) === normalizedActionFragment(previous);
|
|
1197
1219
|
}
|
|
1198
1220
|
|
|
1199
|
-
function previousCheckpointLines(checkpoints: readonly
|
|
1221
|
+
function previousCheckpointLines(checkpoints: readonly ProgressCheckpoint[]): string {
|
|
1200
1222
|
if (checkpoints.length === 0) return "none";
|
|
1201
1223
|
return checkpoints
|
|
1202
1224
|
.slice(-MAX_CONTEXT_CHECKPOINTS)
|
|
1203
|
-
.map((checkpoint) => `- ${
|
|
1225
|
+
.map((checkpoint) => `- ${sanitizeProgressText(checkpoint.text)}`)
|
|
1204
1226
|
.join("\n");
|
|
1205
1227
|
}
|
|
1206
1228
|
|
|
1207
|
-
function formatRawActivity(activity:
|
|
1229
|
+
function formatRawActivity(activity: ProgressActivity): string {
|
|
1208
1230
|
return `- ${activity.text}`;
|
|
1209
1231
|
}
|
|
1210
1232
|
|
|
1211
1233
|
// Strips ANSI/CSI/OSC/DCS/SOS/PM/APC escape sequences from model output before
|
|
1212
|
-
// it lands in the terminal. The
|
|
1234
|
+
// it lands in the terminal. The progress update text is model-controlled and renders into
|
|
1213
1235
|
// the footer; the system prompt asking for plain text is not a security
|
|
1214
1236
|
// boundary, so we defensively remove terminal controls here.
|
|
1215
1237
|
const ESC_BYTE = 0x1b;
|
|
@@ -1315,7 +1337,7 @@ function stripTerminalControls(text: string): string {
|
|
|
1315
1337
|
// parrot them; the regex is narrow enough not to clip natural starts like
|
|
1316
1338
|
// "Activity slowed after retry" or "Checkpointing release state".
|
|
1317
1339
|
const LEAKED_PREFIX_PATTERN =
|
|
1318
|
-
/^\s*(?:[-*•]\s*)?(?:(?:through\s+activity|activity|checkpoint)\s+\d+\s*[:.\-—–]\s*|(?:tldr|summary)\s*[:.\-—–]\s*)+/i;
|
|
1340
|
+
/^\s*(?:[-*•]\s*)?(?:(?:through\s+activity|activity|checkpoint)\s+\d+\s*[:.\-—–]\s*|(?:tldr|summary|progress\s+update|progress)\s*[:.\-—–]\s*)+/i;
|
|
1319
1341
|
const LEADING_PUNCT_PATTERN = /^[\s\-—–•*:#.,;]+/;
|
|
1320
1342
|
const TRAILING_PUNCT_PATTERN = /[\s\-—–•*:#.,;]+$/;
|
|
1321
1343
|
|
|
@@ -1369,6 +1391,15 @@ const DANGLING_PREP_CHAIN_PATTERN =
|
|
|
1369
1391
|
// "Reviewing for image update". Collapse the bare verb+prep into just the verb.
|
|
1370
1392
|
const VERB_BARE_PREP_PATTERN =
|
|
1371
1393
|
/\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;
|
|
1394
|
+
// Identifier strip removes version numbers like "0.3.4" but can leave stranded
|
|
1395
|
+
// release adjectives behind: "Released latest of package" was "Released latest
|
|
1396
|
+
// 0.3.4 of package"; "Released new with X" was "Released new 0.3.4 with X".
|
|
1397
|
+
// The chain pattern keeps the noun phrase that followed the version. The
|
|
1398
|
+
// trailing pattern handles bare "... new" or "... latest" tails.
|
|
1399
|
+
const RELEASE_FRAGMENT_CHAIN_PATTERN =
|
|
1400
|
+
/\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;
|
|
1401
|
+
const RELEASE_FRAGMENT_TRAILING_PATTERN =
|
|
1402
|
+
/\s+(?:new|latest|version|update)\s*(?:of|with|to|and|in|on)?\s*[.!?,;:]?\s*$/i;
|
|
1372
1403
|
|
|
1373
1404
|
function stripDanglingPrepositions(text: string): string {
|
|
1374
1405
|
let cleaned = text;
|
|
@@ -1376,14 +1407,16 @@ function stripDanglingPrepositions(text: string): string {
|
|
|
1376
1407
|
do {
|
|
1377
1408
|
previous = cleaned;
|
|
1378
1409
|
cleaned = cleaned.replace(VERB_BARE_PREP_PATTERN, "$1 ");
|
|
1410
|
+
cleaned = cleaned.replace(RELEASE_FRAGMENT_CHAIN_PATTERN, "$1 ");
|
|
1379
1411
|
cleaned = cleaned.replace(DANGLING_PREP_CHAIN_PATTERN, "$2");
|
|
1412
|
+
cleaned = cleaned.replace(RELEASE_FRAGMENT_TRAILING_PATTERN, "").trim();
|
|
1380
1413
|
cleaned = cleaned.replace(DANGLING_TRAILING_PREP_PATTERN, "").trim();
|
|
1381
1414
|
} while (cleaned !== previous && cleaned.length > 0);
|
|
1382
1415
|
return cleaned;
|
|
1383
1416
|
}
|
|
1384
1417
|
|
|
1385
1418
|
// Last-resort fallback when the model ignored the allow-list and started the
|
|
1386
|
-
//
|
|
1419
|
+
// progress update with a banned tool-narration verb. The map preserves tense (present
|
|
1387
1420
|
// vs past). Case of the first letter follows the original word.
|
|
1388
1421
|
const BANNED_FIRST_WORD_REWRITES: Record<string, string> = {
|
|
1389
1422
|
Reading: "Reviewing",
|
|
@@ -1437,7 +1470,7 @@ function rewriteBannedFirstWord(text: string): string {
|
|
|
1437
1470
|
const original = match[1];
|
|
1438
1471
|
const replacement = BANNED_FIRST_WORD_REWRITES[original];
|
|
1439
1472
|
if (!replacement) return text;
|
|
1440
|
-
// Preserve lowercase-start
|
|
1473
|
+
// Preserve lowercase-start outputs (rare but possible) by matching original case.
|
|
1441
1474
|
const cased =
|
|
1442
1475
|
original[0] === original[0].toLowerCase()
|
|
1443
1476
|
? replacement[0].toLowerCase() + replacement.slice(1)
|
|
@@ -1466,9 +1499,9 @@ function stripSuccessSuffix(text: string): string {
|
|
|
1466
1499
|
return cleaned;
|
|
1467
1500
|
}
|
|
1468
1501
|
|
|
1469
|
-
function
|
|
1502
|
+
function sanitizeProgressText(
|
|
1470
1503
|
text: string,
|
|
1471
|
-
maxChars =
|
|
1504
|
+
maxChars = MAX_SAFE_PROGRESS_CHARS,
|
|
1472
1505
|
): string {
|
|
1473
1506
|
const stripped = stripTerminalControls(text);
|
|
1474
1507
|
const withoutMarkdown = stripMarkdownFormatting(stripped);
|
|
@@ -1576,7 +1609,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1576
1609
|
let statusFilter: StatusFilter = { mode: "all", hidden: new Set() };
|
|
1577
1610
|
const seenStatusKeys = new Set<string>();
|
|
1578
1611
|
const refresh = () => requestRender?.();
|
|
1579
|
-
const
|
|
1612
|
+
const progress = new FooterProgressEngine(refresh);
|
|
1580
1613
|
const restoreStatusFilter = (ctx: ExtensionContext) => {
|
|
1581
1614
|
let restoredFilter = readGlobalStatusFilter();
|
|
1582
1615
|
if (!restoredFilter) {
|
|
@@ -1764,32 +1797,32 @@ export default function (pi: ExtensionAPI) {
|
|
|
1764
1797
|
pi.on("thinking_level_select", async () => refresh());
|
|
1765
1798
|
pi.on("turn_end", async () => refresh());
|
|
1766
1799
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
1767
|
-
|
|
1800
|
+
progress.recordUserMessage(ctx, event.prompt);
|
|
1768
1801
|
});
|
|
1769
1802
|
pi.on("message_update", async (event, ctx) => {
|
|
1770
|
-
|
|
1803
|
+
progress.recordAssistantUpdate(ctx, event.message);
|
|
1771
1804
|
});
|
|
1772
1805
|
pi.on("tool_call", async (event, ctx) => {
|
|
1773
|
-
|
|
1806
|
+
progress.recordToolCall(ctx, event);
|
|
1774
1807
|
});
|
|
1775
1808
|
pi.on("tool_result", async (event, ctx) => {
|
|
1776
|
-
|
|
1809
|
+
progress.recordToolResult(ctx, event);
|
|
1777
1810
|
});
|
|
1778
1811
|
pi.on("message_end", async (event, ctx) => {
|
|
1779
|
-
|
|
1812
|
+
progress.recordMessageEnd(ctx, event.message);
|
|
1780
1813
|
refresh();
|
|
1781
1814
|
});
|
|
1782
1815
|
pi.on("session_before_tree", async () => {
|
|
1783
|
-
|
|
1816
|
+
progress.shutdown();
|
|
1784
1817
|
});
|
|
1785
1818
|
pi.on("session_tree", async (_event, ctx) => {
|
|
1786
|
-
|
|
1819
|
+
progress.startSession(ctx.cwd);
|
|
1787
1820
|
restoreStatusFilter(ctx);
|
|
1788
1821
|
refresh();
|
|
1789
1822
|
});
|
|
1790
1823
|
|
|
1791
1824
|
pi.on("session_start", async (_event, ctx) => {
|
|
1792
|
-
|
|
1825
|
+
progress.startSession(ctx.cwd);
|
|
1793
1826
|
restoreStatusFilter(ctx);
|
|
1794
1827
|
|
|
1795
1828
|
if (!ctx.hasUI) return;
|
|
@@ -1823,13 +1856,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1823
1856
|
const contextText = usage
|
|
1824
1857
|
? `${usage.percent !== null ? `${usage.percent.toFixed(1)}%` : "—%"} / ${formatTokens(usage.contextWindow)}`
|
|
1825
1858
|
: "—";
|
|
1826
|
-
const
|
|
1859
|
+
const progressText = progress.text();
|
|
1827
1860
|
|
|
1828
1861
|
const segmentRenderers: Record<SegmentName, string | null> = {
|
|
1829
1862
|
model: theme.fg("accent", modelName),
|
|
1830
1863
|
thinking: theme.fg(thinkingColor(thinkingLevel), `think:${thinkingLevel}`),
|
|
1831
1864
|
context: theme.fg(contextSegmentColor, contextText),
|
|
1832
|
-
|
|
1865
|
+
progress: progressText ? theme.fg("muted", progressText) : null,
|
|
1833
1866
|
extensions: extensionStatuses ? theme.fg("muted", extensionStatuses) : null,
|
|
1834
1867
|
};
|
|
1835
1868
|
|
|
@@ -1846,7 +1879,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1846
1879
|
});
|
|
1847
1880
|
|
|
1848
1881
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1849
|
-
|
|
1882
|
+
progress.shutdown();
|
|
1850
1883
|
if (ctx.hasUI) ctx.ui.setFooter(undefined);
|
|
1851
1884
|
});
|
|
1852
1885
|
}
|
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.31",
|
|
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.31/assets/screenshot-yellow.png"
|
|
55
56
|
}
|
|
56
57
|
}
|