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 CHANGED
@@ -3,25 +3,25 @@
3
3
  [![npm version](https://img.shields.io/npm/v/pi-bar.svg)](https://www.npmjs.com/package/pi-bar)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/pi-bar.svg)](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 TLDR visible in pi's footer.
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
- ![pi-bar with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.28/assets/screenshot-green.png)
13
- ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.28/assets/screenshot-yellow.png)
14
- ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.28/assets/screenshot-red.png)
12
+ ![pi-bar with low context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.30/assets/screenshot-green.png)
13
+ ![pi-bar with medium context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.30/assets/screenshot-yellow.png)
14
+ ![pi-bar with high context usage](https://cdn.jsdelivr.net/npm/pi-bar@0.3.30/assets/screenshot-red.png)
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 TLDR keeps the current task visible without scrolling.
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 / TLDR / extension-status line.
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,tldr,extensions pi
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`, `tldr`, and `extensions`. The `tldr` segment is hidden until pi-bar generates a current TLDR. The `extensions` segment is hidden when no extension has set a status.
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
- ### Configure live TLDR
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
- pi-bar shows a concise live TLDR of the current task in the footer. It generates TLDRs 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 `tldr` to hide it.
53
+ ### Configure live progress updates
54
54
 
55
- Override the TLDR model with:
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
- PI_BAR_TLDR_MODEL=anthropic/claude-haiku-4-5 pi
60
+ PI_BAR_PROGRESS_MODEL=anthropic/claude-haiku-4-5 pi
59
61
  ```
60
62
 
61
- You can also set `bar.tldrModel` in pi settings.
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 TLDR generation sends short activity snippets to the selected TLDR model provider. Disable the `tldr` segment with `PI_BAR_SHOW` if that is unacceptable.
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> ❯ <tldr> ❯ <extension statuses>
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,tldr,extensions
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" | "tldr" | "extensions";
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 TldrActivityType =
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 TldrDisplayPriority = "immediate" | "normal" | "final";
51
- type TldrActivity = {
50
+ type ProgressDisplayPriority = "immediate" | "normal" | "final";
51
+ type ProgressActivity = {
52
52
  index: number;
53
- activityType: TldrActivityType;
54
- displayPriority: TldrDisplayPriority;
53
+ activityType: ProgressActivityType;
54
+ displayPriority: ProgressDisplayPriority;
55
55
  text: string;
56
56
  toolCallId?: string;
57
57
  };
58
- type TldrCheckpoint = {
58
+ type ProgressCheckpoint = {
59
59
  activityIndex: number;
60
- displayPriority: TldrDisplayPriority;
60
+ displayPriority: ProgressDisplayPriority;
61
61
  text: string;
62
62
  };
63
- type TldrCheckpointJob = {
63
+ type ProgressCheckpointJob = {
64
64
  activityIndex: number;
65
- displayPriority: TldrDisplayPriority;
65
+ displayPriority: ProgressDisplayPriority;
66
66
  runId: number;
67
67
  };
68
- type TldrModelPreference = { provider: string; id: string };
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 SETTINGS_TLDR_KEY = "tldr";
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 TLDR_MAX_TOKENS = 120;
88
- const TLDR_REQUEST_TIMEOUT_MS = 2_000;
89
- const TLDR_DISPLAY_UPDATE_INTERVAL_MS = 1_200;
90
- const TLDR_TARGET_SUMMARY_CHARS = 60;
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 MAX_SAFE_TLDR_CHARS = 240;
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
- "tldr",
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", "tldr", "extensions"].includes(segment),
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 parseTldrModelSpec(value: string): TldrModelPreference | undefined {
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 value = (bar as Record<string, unknown>).tldrModel;
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 tldr = settings[SETTINGS_TLDR_KEY];
225
- if (tldr && typeof tldr === "object" && !Array.isArray(tldr)) {
226
- const value = (tldr as Record<string, unknown>).model;
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 resolveTldrModelPreference(cwd: string): TldrModelPreference | undefined {
234
- const envModel = process.env.PI_BAR_TLDR_MODEL;
235
- if (envModel) return parseTldrModelSpec(envModel);
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 parseTldrModelSpec(projectModel);
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 ? parseTldrModelSpec(globalModel) : undefined;
264
+ return globalModel ? parseProgressModelSpec(globalModel) : undefined;
247
265
  }
248
266
 
249
- // Fallback order optimizes for low latency + low cost first. The TLDR path does
250
- // not need reasoning quality; codex/gpt-5.4-mini is fastest in practice.
251
- const FAST_TLDR_MODELS: readonly TldrModelPreference[] = [
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 formatTldrModelKey(model: TldrModelPreference): string {
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: TldrModelPreference,
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 getFastTldrModelAuth(
294
+ async function getFastProgressModelAuth(
276
295
  ctx: ExtensionContext,
277
- configuredModel?: TldrModelPreference,
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 FAST_TLDR_MODELS) {
285
- if (configuredModel && formatTldrModelKey(candidate) === formatTldrModelKey(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 TldrFactCollector {
472
+ class ProgressFactCollector {
454
473
  private nextIndex = 1;
455
- private readonly activities: TldrActivity[] = [];
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): TldrActivity {
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): TldrActivity | undefined {
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
- }): TldrActivity {
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
- }): TldrActivity {
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): TldrActivity | "emptyFinalStop" | "ignored" {
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 TldrActivity[] {
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: TldrActivityType,
595
- displayPriority: TldrDisplayPriority,
613
+ activityType: ProgressActivityType,
614
+ displayPriority: ProgressDisplayPriority,
596
615
  text: string,
597
616
  toolCallId?: string,
598
- ): TldrActivity {
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 FooterTldrEngine {
616
- private readonly facts = new TldrFactCollector();
617
- private configuredModel?: TldrModelPreference;
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: TldrCheckpoint[] = [];
624
- private checkpointQueue: TldrCheckpointJob[] = [];
625
- private inFlightCheckpoint?: TldrCheckpointJob;
626
- private pendingDisplayCheckpoint?: TldrCheckpoint;
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 TLDR model for intermediate states.
633
- private pendingNormalCheckpoint?: TldrCheckpointJob;
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 = resolveTldrModelPreference(cwd);
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 TLDR context, so wipe both
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: TldrCheckpointJob,
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: TldrActivity): void {
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: TldrCheckpointJob): boolean {
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: TldrCheckpointJob,
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 getFastTldrModelAuth(ctx, this.configuredModel);
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: TLDR_MAX_TOKENS,
913
+ maxTokens: PROGRESS_MAX_TOKENS,
895
914
  maxRetries: 0,
896
915
  cacheRetention: "none",
897
- timeoutMs: TLDR_REQUEST_TIMEOUT_MS,
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 = sanitizeTldrText(rawText);
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: TldrCheckpoint): void {
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: TldrCheckpoint,
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 >= TLDR_DISPLAY_UPDATE_INTERVAL_MS) {
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
- }, TLDR_DISPLAY_UPDATE_INTERVAL_MS - elapsedMs);
983
+ }, PROGRESS_DISPLAY_UPDATE_INTERVAL_MS - elapsedMs);
965
984
  }
966
985
 
967
- private renderCheckpoint(checkpoint: TldrCheckpoint): void {
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 (isNearDuplicateTldr(checkpoint.text, this.lastRenderedText)) {
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: TldrCheckpointJob): UserMessage | undefined {
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 TLDRs (context only, do not copy phrasing):",
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 TLDR.",
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
- function checkpointSystemPrompt(job: TldrCheckpointJob): string {
1012
- // Tense + exemplars vary by priority. Final TLDRs describe completed turns;
1013
- // using -ing examples for final caused all final TLDRs to slip back into
1014
- // present-progressive even though the literal instruction said past tense.
1015
- // Immediate TLDRs cover a brand-new user request; they need a rephrase
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 TLDR regressions",
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. Do not write \"Continuing\", \"Investigating user input\", or any generic filler.";
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 TLDR regressions",
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 TLDR regressions",
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
- return `Write one plain-English TLDR for a Pi coding agent.
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
- Do not start with tool-narration verbs such as Read, Reading, Grep, Listing, Counting, Extracting, Displaying, Editing, Writing, Running, Publishing, Capturing, Verifying, Validating, Checking, Confirming, Searching, Finding.
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 TLDRs for context and the new activity for the update.
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 ${TLDR_TARGET_SUMMARY_CHARS} characters.
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 sanitizeTldrText callers.
1079
- - Continuing with task progression.
1186
+ - Grepping for sanitizeProgressText callers.
1187
+ - Verifying repository status after commit.
1080
1188
  - Investigating user input responses.
1081
1189
 
1082
- ${tenseInstruction}`;
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 TLDRs are near-duplicates if their normalized action fragments (lowercase,
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 isNearDuplicateTldr(current: string, previous: string): boolean {
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 TldrCheckpoint[]): string {
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) => `- ${sanitizeTldrText(checkpoint.text)}`)
1223
+ .map((checkpoint) => `- ${sanitizeProgressText(checkpoint.text)}`)
1113
1224
  .join("\n");
1114
1225
  }
1115
1226
 
1116
- function formatRawActivity(activity: TldrActivity): string {
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 TLDR text is model-controlled and renders into
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 sanitizeTldrText(
1500
+ function sanitizeProgressText(
1310
1501
  text: string,
1311
- maxChars = MAX_SAFE_TLDR_CHARS,
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
- return truncateText(withoutSuccess, maxChars);
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 tldr = new FooterTldrEngine(refresh);
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
- tldr.recordUserMessage(ctx, event.prompt);
1798
+ progress.recordUserMessage(ctx, event.prompt);
1607
1799
  });
1608
1800
  pi.on("message_update", async (event, ctx) => {
1609
- tldr.recordAssistantUpdate(ctx, event.message);
1801
+ progress.recordAssistantUpdate(ctx, event.message);
1610
1802
  });
1611
1803
  pi.on("tool_call", async (event, ctx) => {
1612
- tldr.recordToolCall(ctx, event);
1804
+ progress.recordToolCall(ctx, event);
1613
1805
  });
1614
1806
  pi.on("tool_result", async (event, ctx) => {
1615
- tldr.recordToolResult(ctx, event);
1807
+ progress.recordToolResult(ctx, event);
1616
1808
  });
1617
1809
  pi.on("message_end", async (event, ctx) => {
1618
- tldr.recordMessageEnd(ctx, event.message);
1810
+ progress.recordMessageEnd(ctx, event.message);
1619
1811
  refresh();
1620
1812
  });
1621
1813
  pi.on("session_before_tree", async () => {
1622
- tldr.shutdown();
1814
+ progress.shutdown();
1623
1815
  });
1624
1816
  pi.on("session_tree", async (_event, ctx) => {
1625
- tldr.startSession(ctx.cwd);
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
- tldr.startSession(ctx.cwd);
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 tldrText = tldr.text();
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
- tldr: tldrText ? theme.fg("muted", tldrText) : null,
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
- tldr.shutdown();
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.28",
4
- "description": "Pi footer/statusline showing active model, thinking level, context pressure, live TLDR, and extension statuses.",
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.28/assets/screenshot-yellow.png"
55
+ "image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.30/assets/screenshot-yellow.png"
55
56
  }
56
57
  }