pi-bar 0.3.6 → 0.3.7
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 +14 -11
- package/extensions/status-footer.ts +572 -90
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -9,17 +9,17 @@
|
|
|
9
9
|
◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M ❯ ⓘ Inspecting package structure ❯ ✧ plan:active
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-

|
|
13
13
|
|
|
14
14
|
## Why use it?
|
|
15
15
|
|
|
16
16
|
- **See the active model at a glance** — catch accidental model switches before an expensive or sensitive task starts.
|
|
17
17
|
- **Track thinking level in place** — immediately notice when you are using the wrong reasoning setting.
|
|
18
18
|
- **Watch context pressure early** — context usage turns green, yellow, then red as you approach the limit.
|
|
19
|
-
- **Show
|
|
19
|
+
- **Show live TLDR inline** — pi-bar can generate a compact live summary directly in the footer.
|
|
20
20
|
- **Keep extension statuses visible** — statuses set with `ctx.ui.setStatus()` still appear in the custom footer.
|
|
21
21
|
|
|
22
|
-
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 that refreshes when model, thinking level, context usage,
|
|
22
|
+
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 that refreshes when model, thinking level, context usage, generated TLDR, or extension statuses change.
|
|
23
23
|
|
|
24
24
|
## Install
|
|
25
25
|
|
|
@@ -44,23 +44,25 @@ PI_BAR_SHOW=model,thinking,context,tldr,extensions pi
|
|
|
44
44
|
PI_BAR_SHOW=model,context pi
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
Allowed segments are `model`, `thinking`, `context`, `tldr`, and `extensions`. The `tldr` segment is hidden
|
|
47
|
+
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.
|
|
48
48
|
|
|
49
|
-
###
|
|
49
|
+
### Configure live TLDR
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
pi-bar generates live footer TLDRs from recent prompt, assistant, tool, and result activity. It uses a fast authenticated model and stays quiet when none is available. Limit segment width with:
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
|
|
54
|
+
PI_BAR_TLDR_MAX_WIDTH=48 pi
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
Default is `64`. Set `PI_BAR_SHOW` without `tldr` to hide it.
|
|
58
|
+
|
|
59
|
+
By default, pi-bar uses the first authenticated fast model from its fallback list. Override with:
|
|
58
60
|
|
|
59
61
|
```bash
|
|
60
|
-
|
|
62
|
+
PI_BAR_TLDR_MODEL=anthropic/claude-haiku-4-5 pi
|
|
61
63
|
```
|
|
62
64
|
|
|
63
|
-
|
|
65
|
+
You can also set `bar.tldrModel` in pi settings; `tldr.model` is accepted for compatibility with existing settings.
|
|
64
66
|
|
|
65
67
|
### Configure extension statuses
|
|
66
68
|
|
|
@@ -108,10 +110,11 @@ PI_BAR_ICONS=plain pi
|
|
|
108
110
|
|
|
109
111
|
## Pairs well with
|
|
110
112
|
|
|
111
|
-
- **[pi-tldr](https://www.npmjs.com/package/pi-tldr)** — show live summaries above the input bar; pi-bar mirrors them into the footer.
|
|
112
113
|
- **[pi-chrome](https://www.npmjs.com/package/pi-chrome)** — give your Pi agent your real, signed-in Chrome. Use pi-bar's red-context threshold as the signal to wrap up long browser scrapes before context overflows.
|
|
113
114
|
- **[pi-qq](https://www.npmjs.com/package/pi-qq)** — ask side questions about what the agent just did without polluting the transcript.
|
|
114
115
|
|
|
115
116
|
## Security note
|
|
116
117
|
|
|
117
118
|
Pi extensions run with your local user permissions. Review any pi package source before installing it.
|
|
119
|
+
|
|
120
|
+
Live TLDR generation sends short snippets from recent prompt, assistant, tool, and result activity to the selected TLDR model provider. Disable the `tldr` segment with `PI_BAR_SHOW` if that is unacceptable.
|
|
@@ -20,19 +20,18 @@
|
|
|
20
20
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
21
21
|
import { homedir } from "node:os";
|
|
22
22
|
import { dirname, join } from "node:path";
|
|
23
|
+
import { complete, type UserMessage } from "@earendil-works/pi-ai";
|
|
23
24
|
import {
|
|
24
25
|
getSettingsListTheme,
|
|
26
|
+
SettingsManager,
|
|
25
27
|
type ExtensionAPI,
|
|
26
28
|
type ExtensionContext,
|
|
27
|
-
type Theme,
|
|
28
29
|
type ThemeColor,
|
|
29
30
|
} from "@earendil-works/pi-coding-agent";
|
|
30
31
|
import {
|
|
31
32
|
Container,
|
|
32
|
-
type Component,
|
|
33
33
|
type SettingItem,
|
|
34
34
|
SettingsList,
|
|
35
|
-
type TUI,
|
|
36
35
|
truncateToWidth,
|
|
37
36
|
} from "@earendil-works/pi-tui";
|
|
38
37
|
|
|
@@ -44,17 +43,48 @@ type StatusFilter =
|
|
|
44
43
|
type SerializedStatusFilter =
|
|
45
44
|
| { mode: "all"; hidden: string[] }
|
|
46
45
|
| { mode: "only"; shown: string[] };
|
|
47
|
-
type
|
|
48
|
-
|
|
46
|
+
type TldrActivityType =
|
|
47
|
+
| "user_message"
|
|
48
|
+
| "assistant_update"
|
|
49
|
+
| "tool_call"
|
|
50
|
+
| "tool_result"
|
|
51
|
+
| "assistant_final"
|
|
52
|
+
| "assistant_failure";
|
|
53
|
+
type TldrDisplayPriority = "immediate" | "normal" | "final";
|
|
54
|
+
type TldrActivity = {
|
|
55
|
+
index: number;
|
|
56
|
+
activityType: TldrActivityType;
|
|
57
|
+
displayPriority: TldrDisplayPriority;
|
|
58
|
+
text: string;
|
|
59
|
+
};
|
|
60
|
+
type TldrCheckpoint = {
|
|
61
|
+
activityIndex: number;
|
|
62
|
+
displayPriority: TldrDisplayPriority;
|
|
63
|
+
text: string;
|
|
64
|
+
};
|
|
65
|
+
type TldrCheckpointJob = {
|
|
66
|
+
activityIndex: number;
|
|
67
|
+
displayPriority: TldrDisplayPriority;
|
|
68
|
+
runId: number;
|
|
69
|
+
};
|
|
70
|
+
type TldrModelPreference = { provider: string; id: string };
|
|
71
|
+
type FastModelAuth = {
|
|
72
|
+
model: Parameters<typeof complete>[0];
|
|
73
|
+
apiKey: string;
|
|
74
|
+
headers?: Record<string, string>;
|
|
49
75
|
};
|
|
50
|
-
type CapturedTldrWidget =
|
|
51
|
-
| { kind: "lines"; lines: string[] }
|
|
52
|
-
| { kind: "factory"; factory: TldrWidgetFactory };
|
|
53
76
|
|
|
54
77
|
const STATUS_FILTER_ENTRY_TYPE = "pi-bar-status-filter";
|
|
55
|
-
const TLDR_WIDGET_KEY = "pi-tldr";
|
|
56
78
|
const DEFAULT_TLDR_MAX_WIDTH = 64;
|
|
57
|
-
const
|
|
79
|
+
const SETTINGS_TLDR_KEY = "tldr";
|
|
80
|
+
const SETTINGS_BAR_KEY = "bar";
|
|
81
|
+
const MAX_ACTIVITY_TEXT_CHARS = 1_500;
|
|
82
|
+
const MAX_RETAINED_RAW_ACTIVITIES = 128;
|
|
83
|
+
const MAX_CONTEXT_CHECKPOINTS = 8;
|
|
84
|
+
const TLDR_MAX_TOKENS = 120;
|
|
85
|
+
const TLDR_REQUEST_TIMEOUT_MS = 2_000;
|
|
86
|
+
const TLDR_DISPLAY_UPDATE_INTERVAL_MS = 1_200;
|
|
87
|
+
const TLDR_TARGET_SUMMARY_CHARS = 80;
|
|
58
88
|
const CONFIG_PATH =
|
|
59
89
|
process.env.PI_BAR_CONFIG ?? join(homedir(), ".pi", "agent", "pi-bar.json");
|
|
60
90
|
|
|
@@ -221,52 +251,108 @@ function parseTldrMaxWidth(): number {
|
|
|
221
251
|
return DEFAULT_TLDR_MAX_WIDTH;
|
|
222
252
|
}
|
|
223
253
|
|
|
224
|
-
function
|
|
225
|
-
|
|
254
|
+
function parseTldrModelSpec(value: string): TldrModelPreference | undefined {
|
|
255
|
+
const trimmed = value.trim();
|
|
256
|
+
if (!trimmed || trimmed === "auto") return undefined;
|
|
257
|
+
const separator = trimmed.indexOf("/");
|
|
258
|
+
if (separator <= 0 || separator === trimmed.length - 1) return undefined;
|
|
259
|
+
return { provider: trimmed.slice(0, separator), id: trimmed.slice(separator + 1) };
|
|
226
260
|
}
|
|
227
261
|
|
|
228
|
-
function
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
})
|
|
241
|
-
.filter(Boolean);
|
|
262
|
+
function settingsModelValue(settings: Record<string, unknown>): string | undefined {
|
|
263
|
+
const bar = settings[SETTINGS_BAR_KEY];
|
|
264
|
+
if (bar && typeof bar === "object" && !Array.isArray(bar)) {
|
|
265
|
+
const value = (bar as Record<string, unknown>).tldrModel;
|
|
266
|
+
if (typeof value === "string") return value;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const tldr = settings[SETTINGS_TLDR_KEY];
|
|
270
|
+
if (tldr && typeof tldr === "object" && !Array.isArray(tldr)) {
|
|
271
|
+
const value = (tldr as Record<string, unknown>).model;
|
|
272
|
+
if (typeof value === "string") return value;
|
|
273
|
+
}
|
|
242
274
|
|
|
243
|
-
|
|
244
|
-
return text.length > 0 ? text : null;
|
|
275
|
+
return undefined;
|
|
245
276
|
}
|
|
246
277
|
|
|
247
|
-
function
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
theme: Theme,
|
|
251
|
-
width: number,
|
|
252
|
-
maxWidth: number,
|
|
253
|
-
): string | null {
|
|
254
|
-
if (!widget) return null;
|
|
278
|
+
function resolveTldrModelPreference(cwd: string): TldrModelPreference | undefined {
|
|
279
|
+
const envModel = process.env.PI_BAR_TLDR_MODEL;
|
|
280
|
+
if (envModel) return parseTldrModelSpec(envModel);
|
|
255
281
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
282
|
+
const settings = SettingsManager.create(cwd);
|
|
283
|
+
const projectModel = settingsModelValue(
|
|
284
|
+
settings.getProjectSettings() as Record<string, unknown>,
|
|
285
|
+
);
|
|
286
|
+
if (projectModel !== undefined) return parseTldrModelSpec(projectModel);
|
|
287
|
+
|
|
288
|
+
const globalModel = settingsModelValue(
|
|
289
|
+
settings.getGlobalSettings() as Record<string, unknown>,
|
|
290
|
+
);
|
|
291
|
+
return globalModel ? parseTldrModelSpec(globalModel) : undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const FAST_TLDR_MODELS: readonly TldrModelPreference[] = [
|
|
295
|
+
{ provider: "anthropic", id: "claude-haiku-4-5" },
|
|
296
|
+
{ provider: "anthropic", id: "claude-haiku-4-5-20251001" },
|
|
297
|
+
{ provider: "openai-codex", id: "gpt-5.4-mini" },
|
|
298
|
+
{ provider: "openai-codex", id: "gpt-5.3-codex-spark" },
|
|
299
|
+
];
|
|
300
|
+
|
|
301
|
+
function formatTldrModelKey(model: TldrModelPreference): string {
|
|
302
|
+
return `${model.provider}/${model.id}`;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function getModelAuth(
|
|
306
|
+
ctx: ExtensionContext,
|
|
307
|
+
preference: TldrModelPreference,
|
|
308
|
+
): Promise<FastModelAuth | undefined> {
|
|
309
|
+
const model = ctx.modelRegistry.find(preference.provider, preference.id);
|
|
310
|
+
if (!model) return undefined;
|
|
311
|
+
|
|
312
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
|
|
313
|
+
return auth.ok && auth.apiKey
|
|
314
|
+
? { model, apiKey: auth.apiKey, headers: auth.headers }
|
|
315
|
+
: undefined;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function getFastTldrModelAuth(
|
|
319
|
+
ctx: ExtensionContext,
|
|
320
|
+
configuredModel?: TldrModelPreference,
|
|
321
|
+
): Promise<FastModelAuth | undefined> {
|
|
322
|
+
if (configuredModel) {
|
|
323
|
+
const configuredAuth = await getModelAuth(ctx, configuredModel);
|
|
324
|
+
if (configuredAuth) return configuredAuth;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
for (const candidate of FAST_TLDR_MODELS) {
|
|
328
|
+
if (configuredModel && formatTldrModelKey(candidate) === formatTldrModelKey(configuredModel)) {
|
|
329
|
+
continue;
|
|
265
330
|
}
|
|
331
|
+
const auth = await getModelAuth(ctx, candidate);
|
|
332
|
+
if (auth) return auth;
|
|
266
333
|
}
|
|
267
334
|
|
|
268
|
-
|
|
269
|
-
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function truncateText(text: string, maxChars: number): string {
|
|
339
|
+
if (text.length <= maxChars) return text;
|
|
340
|
+
return `${text.slice(0, maxChars - 1)}…`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function extractTextContent(content: readonly unknown[] | undefined): string | undefined {
|
|
344
|
+
if (!content) return undefined;
|
|
345
|
+
const text = content
|
|
346
|
+
.map((part) => {
|
|
347
|
+
if (!part || typeof part !== "object") return undefined;
|
|
348
|
+
const record = part as Record<string, unknown>;
|
|
349
|
+
return record.type === "text" && typeof record.text === "string"
|
|
350
|
+
? record.text
|
|
351
|
+
: undefined;
|
|
352
|
+
})
|
|
353
|
+
.filter((part): part is string => Boolean(part))
|
|
354
|
+
.join("\n");
|
|
355
|
+
return text.length > 0 ? text : undefined;
|
|
270
356
|
}
|
|
271
357
|
|
|
272
358
|
function prefixed(icon: string, text: string, preset: IconPreset): string {
|
|
@@ -274,6 +360,425 @@ function prefixed(icon: string, text: string, preset: IconPreset): string {
|
|
|
274
360
|
return `${icon} ${text}`;
|
|
275
361
|
}
|
|
276
362
|
|
|
363
|
+
class TldrFactCollector {
|
|
364
|
+
private nextIndex = 1;
|
|
365
|
+
private readonly activities: TldrActivity[] = [];
|
|
366
|
+
|
|
367
|
+
resetConversation(): void {
|
|
368
|
+
this.nextIndex = 1;
|
|
369
|
+
this.activities.splice(0);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
recordUserMessage(prompt: string): TldrActivity {
|
|
373
|
+
return this.addActivity("user_message", "immediate", `User message: ${prompt}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
recordAssistantUpdate(message: unknown): TldrActivity | undefined {
|
|
377
|
+
if (!message || typeof message !== "object") return undefined;
|
|
378
|
+
const record = message as Record<string, unknown>;
|
|
379
|
+
if (record.role !== "assistant") return undefined;
|
|
380
|
+
const text = extractTextContent(record.content as readonly unknown[] | undefined);
|
|
381
|
+
return text
|
|
382
|
+
? this.addActivity("assistant_update", "normal", `Assistant update: ${text}`)
|
|
383
|
+
: undefined;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
recordToolCall(event: { toolName: string }): TldrActivity {
|
|
387
|
+
return this.addActivity("tool_call", "normal", `Tool started: ${event.toolName}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
recordToolResult(event: {
|
|
391
|
+
toolName: string;
|
|
392
|
+
isError?: boolean;
|
|
393
|
+
content?: readonly unknown[];
|
|
394
|
+
}): TldrActivity {
|
|
395
|
+
const resultText = extractTextContent(event.content);
|
|
396
|
+
const status = event.isError ? "error" : "ok";
|
|
397
|
+
return this.addActivity(
|
|
398
|
+
"tool_result",
|
|
399
|
+
"normal",
|
|
400
|
+
resultText
|
|
401
|
+
? `Tool finished: ${event.toolName} (${status})\n${resultText}`
|
|
402
|
+
: `Tool finished: ${event.toolName} (${status})`,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
recordMessageEnd(message: unknown): TldrActivity | "emptyFinalStop" | "ignored" {
|
|
407
|
+
if (!message || typeof message !== "object") return "ignored";
|
|
408
|
+
const record = message as Record<string, unknown>;
|
|
409
|
+
if (record.role !== "assistant") return "ignored";
|
|
410
|
+
|
|
411
|
+
const stopReason = typeof record.stopReason === "string" ? record.stopReason : undefined;
|
|
412
|
+
if (stopReason === "toolUse") return "ignored";
|
|
413
|
+
|
|
414
|
+
let text: string | undefined;
|
|
415
|
+
if (stopReason === "stop") {
|
|
416
|
+
const finalText = extractTextContent(record.content as readonly unknown[] | undefined);
|
|
417
|
+
if (!finalText) return "emptyFinalStop";
|
|
418
|
+
text = `Assistant final response: ${finalText}`;
|
|
419
|
+
} else if (stopReason) {
|
|
420
|
+
const error = typeof record.errorMessage === "string" ? record.errorMessage : undefined;
|
|
421
|
+
text = error
|
|
422
|
+
? `Assistant finished with ${stopReason}: ${error}`
|
|
423
|
+
: `Assistant finished with ${stopReason}`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return text
|
|
427
|
+
? this.addActivity(
|
|
428
|
+
stopReason === "stop" ? "assistant_final" : "assistant_failure",
|
|
429
|
+
"final",
|
|
430
|
+
text,
|
|
431
|
+
)
|
|
432
|
+
: "ignored";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
activitiesAfter(previousIndex: number, throughIndex: number): readonly TldrActivity[] {
|
|
436
|
+
return this.activities.filter(
|
|
437
|
+
(activity) => activity.index > previousIndex && activity.index <= throughIndex,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
latestActivityIndex(): number {
|
|
442
|
+
return this.nextIndex - 1;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
discardActivitiesThrough(activityIndex: number): void {
|
|
446
|
+
const firstRetainedIndex = this.activities.findIndex(
|
|
447
|
+
(activity) => activity.index > activityIndex,
|
|
448
|
+
);
|
|
449
|
+
if (firstRetainedIndex === -1) {
|
|
450
|
+
this.activities.splice(0);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (firstRetainedIndex > 0) this.activities.splice(0, firstRetainedIndex);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private addActivity(
|
|
457
|
+
activityType: TldrActivityType,
|
|
458
|
+
displayPriority: TldrDisplayPriority,
|
|
459
|
+
text: string,
|
|
460
|
+
): TldrActivity {
|
|
461
|
+
const activity = {
|
|
462
|
+
index: this.nextIndex,
|
|
463
|
+
activityType,
|
|
464
|
+
displayPriority,
|
|
465
|
+
text: truncateText(text, MAX_ACTIVITY_TEXT_CHARS),
|
|
466
|
+
};
|
|
467
|
+
this.nextIndex++;
|
|
468
|
+
this.activities.push(activity);
|
|
469
|
+
if (this.activities.length > MAX_RETAINED_RAW_ACTIVITIES) {
|
|
470
|
+
this.activities.splice(0, this.activities.length - MAX_RETAINED_RAW_ACTIVITIES);
|
|
471
|
+
}
|
|
472
|
+
return activity;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
class FooterTldrEngine {
|
|
477
|
+
private readonly facts = new TldrFactCollector();
|
|
478
|
+
private configuredModel?: TldrModelPreference;
|
|
479
|
+
private runId = 0;
|
|
480
|
+
private latestAcceptedActivityIndex = 0;
|
|
481
|
+
private lastRenderedActivityIndex = 0;
|
|
482
|
+
private lastRenderedText = "";
|
|
483
|
+
private lastDisplayAt = Number.NEGATIVE_INFINITY;
|
|
484
|
+
private readonly acceptedCheckpoints: TldrCheckpoint[] = [];
|
|
485
|
+
private checkpointQueue: TldrCheckpointJob[] = [];
|
|
486
|
+
private inFlightCheckpoint?: TldrCheckpointJob;
|
|
487
|
+
private pendingDisplayCheckpoint?: TldrCheckpoint;
|
|
488
|
+
private displayTimer?: ReturnType<typeof setTimeout>;
|
|
489
|
+
private abortController?: AbortController;
|
|
490
|
+
private currentText: string | null = null;
|
|
491
|
+
|
|
492
|
+
constructor(private readonly requestRender: () => void) {}
|
|
493
|
+
|
|
494
|
+
text(maxWidth: number): string | null {
|
|
495
|
+
return this.currentText ? truncateToWidth(this.currentText, maxWidth) : null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
startSession(cwd: string): void {
|
|
499
|
+
this.configuredModel = resolveTldrModelPreference(cwd);
|
|
500
|
+
this.facts.resetConversation();
|
|
501
|
+
this.startFreshRun();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
shutdown(): void {
|
|
505
|
+
this.facts.resetConversation();
|
|
506
|
+
this.startFreshRun();
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
recordUserMessage(ctx: ExtensionContext, prompt: string): void {
|
|
510
|
+
this.currentText = null;
|
|
511
|
+
this.requestRender();
|
|
512
|
+
this.enqueue(ctx, this.facts.recordUserMessage(prompt));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
recordAssistantUpdate(ctx: ExtensionContext, message: unknown): void {
|
|
516
|
+
const activity = this.facts.recordAssistantUpdate(message);
|
|
517
|
+
if (activity) this.enqueue(ctx, activity);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
recordToolCall(ctx: ExtensionContext, event: { toolName: string }): void {
|
|
521
|
+
this.enqueue(ctx, this.facts.recordToolCall(event));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
recordToolResult(
|
|
525
|
+
ctx: ExtensionContext,
|
|
526
|
+
event: { toolName: string; isError?: boolean; content?: readonly unknown[] },
|
|
527
|
+
): void {
|
|
528
|
+
this.enqueue(ctx, this.facts.recordToolResult(event));
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
recordMessageEnd(ctx: ExtensionContext, message: unknown): void {
|
|
532
|
+
const result = this.facts.recordMessageEnd(message);
|
|
533
|
+
if (result === "ignored") return;
|
|
534
|
+
if (result === "emptyFinalStop") {
|
|
535
|
+
this.facts.resetConversation();
|
|
536
|
+
this.startFreshRun();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this.enqueue(ctx, result);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
private startFreshRun(): void {
|
|
543
|
+
this.runId++;
|
|
544
|
+
this.cancelWork();
|
|
545
|
+
this.latestAcceptedActivityIndex = 0;
|
|
546
|
+
this.lastRenderedActivityIndex = 0;
|
|
547
|
+
this.lastRenderedText = "";
|
|
548
|
+
this.lastDisplayAt = Number.NEGATIVE_INFINITY;
|
|
549
|
+
this.acceptedCheckpoints.splice(0);
|
|
550
|
+
this.pendingDisplayCheckpoint = undefined;
|
|
551
|
+
this.currentText = null;
|
|
552
|
+
this.requestRender();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
private cancelWork(): void {
|
|
556
|
+
if (this.displayTimer) clearTimeout(this.displayTimer);
|
|
557
|
+
this.displayTimer = undefined;
|
|
558
|
+
this.checkpointQueue.splice(0);
|
|
559
|
+
this.inFlightCheckpoint = undefined;
|
|
560
|
+
this.abortController?.abort();
|
|
561
|
+
this.abortController = undefined;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private enqueue(ctx: ExtensionContext, activity: TldrActivity): void {
|
|
565
|
+
if (!ctx.hasUI) return;
|
|
566
|
+
const job = {
|
|
567
|
+
activityIndex: activity.index,
|
|
568
|
+
displayPriority: activity.displayPriority,
|
|
569
|
+
runId: this.runId,
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
if (job.displayPriority === "immediate") {
|
|
573
|
+
this.clearPendingDisplay();
|
|
574
|
+
this.lastRenderedText = "";
|
|
575
|
+
this.checkpointQueue.splice(0);
|
|
576
|
+
this.abortInFlightCheckpoint();
|
|
577
|
+
this.checkpointQueue.push(job);
|
|
578
|
+
} else if (job.displayPriority === "final") {
|
|
579
|
+
this.clearPendingDisplay();
|
|
580
|
+
this.checkpointQueue = this.checkpointQueue.filter(
|
|
581
|
+
(queued) => queued.displayPriority !== "normal",
|
|
582
|
+
);
|
|
583
|
+
if (this.inFlightCheckpoint?.displayPriority === "normal") {
|
|
584
|
+
this.abortInFlightCheckpoint();
|
|
585
|
+
}
|
|
586
|
+
this.checkpointQueue.push(job);
|
|
587
|
+
} else {
|
|
588
|
+
this.checkpointQueue = this.checkpointQueue.filter(
|
|
589
|
+
(queued) => queued.displayPriority !== "normal",
|
|
590
|
+
);
|
|
591
|
+
this.checkpointQueue.push(job);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
this.pump(ctx);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
private clearPendingDisplay(): void {
|
|
598
|
+
if (this.displayTimer) clearTimeout(this.displayTimer);
|
|
599
|
+
this.displayTimer = undefined;
|
|
600
|
+
this.pendingDisplayCheckpoint = undefined;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private abortInFlightCheckpoint(): void {
|
|
604
|
+
if (!this.inFlightCheckpoint) return;
|
|
605
|
+
this.abortController?.abort();
|
|
606
|
+
this.abortController = undefined;
|
|
607
|
+
this.inFlightCheckpoint = undefined;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private pump(ctx: ExtensionContext): void {
|
|
611
|
+
if (!ctx.hasUI || this.inFlightCheckpoint) return;
|
|
612
|
+
const job = this.checkpointQueue.shift();
|
|
613
|
+
if (!job) return;
|
|
614
|
+
if (job.runId !== this.runId || job.activityIndex <= this.latestAcceptedActivityIndex) {
|
|
615
|
+
this.pump(ctx);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
this.inFlightCheckpoint = job;
|
|
619
|
+
void this.runCheckpointRequest(ctx, job);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private isCurrentJob(job: TldrCheckpointJob): boolean {
|
|
623
|
+
return job.runId === this.runId && this.inFlightCheckpoint === job;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
private async runCheckpointRequest(
|
|
627
|
+
ctx: ExtensionContext,
|
|
628
|
+
job: TldrCheckpointJob,
|
|
629
|
+
): Promise<void> {
|
|
630
|
+
let abortController: AbortController | undefined;
|
|
631
|
+
try {
|
|
632
|
+
const prompt = this.checkpointPrompt(job);
|
|
633
|
+
if (!prompt) return;
|
|
634
|
+
const auth = await getFastTldrModelAuth(ctx, this.configuredModel);
|
|
635
|
+
if (!this.isCurrentJob(job) || !auth) return;
|
|
636
|
+
|
|
637
|
+
abortController = new AbortController();
|
|
638
|
+
this.abortController = abortController;
|
|
639
|
+
const response = await complete(
|
|
640
|
+
auth.model,
|
|
641
|
+
{ systemPrompt: checkpointSystemPrompt(job), messages: [prompt] },
|
|
642
|
+
{
|
|
643
|
+
apiKey: auth.apiKey,
|
|
644
|
+
headers: auth.headers,
|
|
645
|
+
maxTokens: TLDR_MAX_TOKENS,
|
|
646
|
+
maxRetries: 0,
|
|
647
|
+
cacheRetention: "none",
|
|
648
|
+
timeoutMs: TLDR_REQUEST_TIMEOUT_MS,
|
|
649
|
+
signal: abortController.signal,
|
|
650
|
+
},
|
|
651
|
+
);
|
|
652
|
+
if (!this.isCurrentJob(job) || response.stopReason !== "stop") return;
|
|
653
|
+
|
|
654
|
+
const text = extractTextContent(response.content) ?? "";
|
|
655
|
+
if (!text) return;
|
|
656
|
+
const checkpoint = {
|
|
657
|
+
activityIndex: job.activityIndex,
|
|
658
|
+
displayPriority: job.displayPriority,
|
|
659
|
+
text,
|
|
660
|
+
};
|
|
661
|
+
this.acceptCheckpoint(checkpoint);
|
|
662
|
+
this.considerDisplayingCheckpoint(ctx, checkpoint);
|
|
663
|
+
} catch {
|
|
664
|
+
// Best effort. Later checkpoints include unaccepted raw activity.
|
|
665
|
+
} finally {
|
|
666
|
+
if (abortController && this.abortController === abortController) {
|
|
667
|
+
this.abortController = undefined;
|
|
668
|
+
}
|
|
669
|
+
if (this.inFlightCheckpoint === job) this.inFlightCheckpoint = undefined;
|
|
670
|
+
this.pump(ctx);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
private acceptCheckpoint(checkpoint: TldrCheckpoint): void {
|
|
675
|
+
this.acceptedCheckpoints.push(checkpoint);
|
|
676
|
+
if (this.acceptedCheckpoints.length > MAX_CONTEXT_CHECKPOINTS) {
|
|
677
|
+
this.acceptedCheckpoints.splice(
|
|
678
|
+
0,
|
|
679
|
+
this.acceptedCheckpoints.length - MAX_CONTEXT_CHECKPOINTS,
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
this.latestAcceptedActivityIndex = checkpoint.activityIndex;
|
|
683
|
+
this.facts.discardActivitiesThrough(checkpoint.activityIndex);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
private considerDisplayingCheckpoint(
|
|
687
|
+
ctx: ExtensionContext,
|
|
688
|
+
checkpoint: TldrCheckpoint,
|
|
689
|
+
): void {
|
|
690
|
+
if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
|
|
691
|
+
if (checkpoint.activityIndex !== this.facts.latestActivityIndex()) return;
|
|
692
|
+
|
|
693
|
+
if (checkpoint.displayPriority !== "normal" || !this.lastRenderedText) {
|
|
694
|
+
this.clearPendingDisplay();
|
|
695
|
+
this.renderCheckpoint(checkpoint);
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const elapsedMs = performance.now() - this.lastDisplayAt;
|
|
700
|
+
if (elapsedMs >= TLDR_DISPLAY_UPDATE_INTERVAL_MS) {
|
|
701
|
+
this.clearPendingDisplay();
|
|
702
|
+
this.renderCheckpoint(checkpoint);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
this.pendingDisplayCheckpoint = checkpoint;
|
|
707
|
+
if (this.displayTimer) return;
|
|
708
|
+
this.displayTimer = setTimeout(() => {
|
|
709
|
+
this.displayTimer = undefined;
|
|
710
|
+
const pending = this.pendingDisplayCheckpoint;
|
|
711
|
+
this.pendingDisplayCheckpoint = undefined;
|
|
712
|
+
if (!pending || pending.activityIndex !== this.facts.latestActivityIndex()) return;
|
|
713
|
+
this.renderCheckpoint(pending);
|
|
714
|
+
}, TLDR_DISPLAY_UPDATE_INTERVAL_MS - elapsedMs);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
private renderCheckpoint(checkpoint: TldrCheckpoint): void {
|
|
718
|
+
if (checkpoint.activityIndex <= this.lastRenderedActivityIndex) return;
|
|
719
|
+
if (checkpoint.text === this.lastRenderedText) return;
|
|
720
|
+
this.lastRenderedActivityIndex = checkpoint.activityIndex;
|
|
721
|
+
this.lastRenderedText = checkpoint.text;
|
|
722
|
+
this.lastDisplayAt = performance.now();
|
|
723
|
+
this.currentText = checkpoint.text;
|
|
724
|
+
this.requestRender();
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private checkpointPrompt(job: TldrCheckpointJob): UserMessage | undefined {
|
|
728
|
+
const rawActivities = this.facts.activitiesAfter(
|
|
729
|
+
this.latestAcceptedActivityIndex,
|
|
730
|
+
job.activityIndex,
|
|
731
|
+
);
|
|
732
|
+
if (rawActivities.length === 0) return undefined;
|
|
733
|
+
return {
|
|
734
|
+
role: "user",
|
|
735
|
+
content: [
|
|
736
|
+
{
|
|
737
|
+
type: "text",
|
|
738
|
+
text: [
|
|
739
|
+
"Previous generated TLDR checkpoints:",
|
|
740
|
+
previousCheckpointLines(this.acceptedCheckpoints),
|
|
741
|
+
"",
|
|
742
|
+
"New raw activity since the latest accepted checkpoint:",
|
|
743
|
+
...rawActivities.map(formatRawActivity),
|
|
744
|
+
"",
|
|
745
|
+
`Write the next TLDR through activity ${job.activityIndex}.`,
|
|
746
|
+
].join("\n"),
|
|
747
|
+
},
|
|
748
|
+
],
|
|
749
|
+
timestamp: Date.now(),
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function checkpointSystemPrompt(job: TldrCheckpointJob): string {
|
|
755
|
+
const tenseInstruction =
|
|
756
|
+
job.displayPriority === "final"
|
|
757
|
+
? "Start with a past-tense verb."
|
|
758
|
+
: "Start with a present-tense -ing verb.";
|
|
759
|
+
return `Write one plain-English TLDR for a terminal coding agent.
|
|
760
|
+
Use the prior TLDRs for context and the new activity for the update.
|
|
761
|
+
Summarize only activity up to the requested index.
|
|
762
|
+
If context is sparse, still summarize the available activity.
|
|
763
|
+
Never ask for more information or say there is not enough context.
|
|
764
|
+
Return exactly one complete sentence under ${TLDR_TARGET_SUMMARY_CHARS} characters.
|
|
765
|
+
Use third person. Do not address the user.
|
|
766
|
+
Plain text only; no markdown, JSON, code, bullets, quotes, or file/tool names.
|
|
767
|
+
${tenseInstruction}`;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function previousCheckpointLines(checkpoints: readonly TldrCheckpoint[]): string {
|
|
771
|
+
if (checkpoints.length === 0) return "none";
|
|
772
|
+
return checkpoints
|
|
773
|
+
.slice(-MAX_CONTEXT_CHECKPOINTS)
|
|
774
|
+
.map((checkpoint) => `- Through activity ${checkpoint.activityIndex}: ${checkpoint.text}`)
|
|
775
|
+
.join("\n");
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function formatRawActivity(activity: TldrActivity): string {
|
|
779
|
+
return `[${activity.index}] ${activity.activityType}: ${activity.text}`;
|
|
780
|
+
}
|
|
781
|
+
|
|
277
782
|
function shouldShowStatus(key: string, filter: StatusFilter): boolean {
|
|
278
783
|
if (filter.mode === "only") return filter.shown.has(key);
|
|
279
784
|
return !filter.hidden.has(key);
|
|
@@ -368,39 +873,9 @@ function getKnownStatusKeys(filter: StatusFilter, seenStatusKeys: Set<string>):
|
|
|
368
873
|
export default function (pi: ExtensionAPI) {
|
|
369
874
|
let requestRender: (() => void) | undefined;
|
|
370
875
|
let statusFilter: StatusFilter = { mode: "all", hidden: new Set() };
|
|
371
|
-
let capturedTldrWidget: CapturedTldrWidget | undefined;
|
|
372
876
|
const seenStatusKeys = new Set<string>();
|
|
373
|
-
const patchedUis = new WeakSet<object>();
|
|
374
877
|
const refresh = () => requestRender?.();
|
|
375
|
-
const
|
|
376
|
-
if (!ctx.hasUI) return;
|
|
377
|
-
const ui = ctx.ui as ExtensionContext["ui"];
|
|
378
|
-
if (patchedUis.has(ui)) return;
|
|
379
|
-
patchedUis.add(ui);
|
|
380
|
-
|
|
381
|
-
const originalSetWidget = ui.setWidget.bind(ui);
|
|
382
|
-
ui.setWidget = ((key: string, content: unknown, options?: unknown) => {
|
|
383
|
-
if (key === TLDR_WIDGET_KEY) {
|
|
384
|
-
if (Array.isArray(content)) {
|
|
385
|
-
capturedTldrWidget = { kind: "lines", lines: content };
|
|
386
|
-
} else if (typeof content === "function") {
|
|
387
|
-
capturedTldrWidget = {
|
|
388
|
-
kind: "factory",
|
|
389
|
-
factory: content as TldrWidgetFactory,
|
|
390
|
-
};
|
|
391
|
-
} else {
|
|
392
|
-
capturedTldrWidget = undefined;
|
|
393
|
-
}
|
|
394
|
-
refresh();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return originalSetWidget(
|
|
398
|
-
key,
|
|
399
|
-
content as Parameters<ExtensionContext["ui"]["setWidget"]>[1],
|
|
400
|
-
options as Parameters<ExtensionContext["ui"]["setWidget"]>[2],
|
|
401
|
-
);
|
|
402
|
-
}) as ExtensionContext["ui"]["setWidget"];
|
|
403
|
-
};
|
|
878
|
+
const tldr = new FooterTldrEngine(refresh);
|
|
404
879
|
const restoreStatusFilter = (ctx: ExtensionContext) => {
|
|
405
880
|
let restoredFilter = readGlobalStatusFilter();
|
|
406
881
|
if (!restoredFilter) {
|
|
@@ -587,16 +1062,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
587
1062
|
pi.on("model_select", async () => refresh());
|
|
588
1063
|
pi.on("thinking_level_select", async () => refresh());
|
|
589
1064
|
pi.on("turn_end", async () => refresh());
|
|
590
|
-
pi.on("
|
|
1065
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
1066
|
+
tldr.recordUserMessage(ctx, event.prompt);
|
|
1067
|
+
});
|
|
1068
|
+
pi.on("message_update", async (event, ctx) => {
|
|
1069
|
+
tldr.recordAssistantUpdate(ctx, event.message);
|
|
1070
|
+
});
|
|
1071
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
1072
|
+
tldr.recordToolCall(ctx, event);
|
|
1073
|
+
});
|
|
1074
|
+
pi.on("tool_result", async (event, ctx) => {
|
|
1075
|
+
tldr.recordToolResult(ctx, event);
|
|
1076
|
+
});
|
|
1077
|
+
pi.on("message_end", async (event, ctx) => {
|
|
1078
|
+
tldr.recordMessageEnd(ctx, event.message);
|
|
1079
|
+
refresh();
|
|
1080
|
+
});
|
|
591
1081
|
pi.on("session_tree", async (_event, ctx) => {
|
|
592
|
-
installTldrWidgetCapture(ctx);
|
|
593
1082
|
restoreStatusFilter(ctx);
|
|
594
1083
|
refresh();
|
|
595
1084
|
});
|
|
596
1085
|
|
|
597
1086
|
pi.on("session_start", async (_event, ctx) => {
|
|
598
|
-
|
|
599
|
-
installTldrWidgetCapture(ctx);
|
|
1087
|
+
tldr.startSession(ctx.cwd);
|
|
600
1088
|
restoreStatusFilter(ctx);
|
|
601
1089
|
|
|
602
1090
|
if (!ctx.hasUI) return;
|
|
@@ -633,13 +1121,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
633
1121
|
const contextText = usage
|
|
634
1122
|
? `${usage.percent !== null ? `${usage.percent.toFixed(1)}%` : "—%"} / ${formatTokens(usage.contextWindow)}`
|
|
635
1123
|
: "—";
|
|
636
|
-
const tldrText =
|
|
637
|
-
capturedTldrWidget,
|
|
638
|
-
tui,
|
|
639
|
-
theme,
|
|
640
|
-
width,
|
|
641
|
-
tldrMaxWidth,
|
|
642
|
-
);
|
|
1124
|
+
const tldrText = tldr.text(tldrMaxWidth);
|
|
643
1125
|
|
|
644
1126
|
const segmentRenderers: Record<SegmentName, string | null> = {
|
|
645
1127
|
model: theme.fg(
|
|
@@ -678,7 +1160,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
678
1160
|
});
|
|
679
1161
|
|
|
680
1162
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
681
|
-
|
|
1163
|
+
tldr.shutdown();
|
|
682
1164
|
if (ctx.hasUI) ctx.ui.setFooter(undefined);
|
|
683
1165
|
});
|
|
684
1166
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-bar",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "Never accidentally run Opus on a typo. Pi footer/statusline showing active model, thinking level, context usage,
|
|
3
|
+
"version": "0.3.7",
|
|
4
|
+
"description": "Never accidentally run Opus on a typo. Pi footer/statusline showing active model, thinking level, context usage, live TLDR, and extension statuses.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"pi-package",
|
|
@@ -14,8 +14,7 @@
|
|
|
14
14
|
"thinking",
|
|
15
15
|
"context",
|
|
16
16
|
"context-window",
|
|
17
|
-
"tldr"
|
|
18
|
-
"pi-tldr"
|
|
17
|
+
"tldr"
|
|
19
18
|
],
|
|
20
19
|
"license": "MIT",
|
|
21
20
|
"repository": {
|
|
@@ -33,10 +32,14 @@
|
|
|
33
32
|
"LICENSE"
|
|
34
33
|
],
|
|
35
34
|
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-ai": "*",
|
|
36
36
|
"@earendil-works/pi-coding-agent": "*",
|
|
37
37
|
"@earendil-works/pi-tui": "*"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
|
+
"@earendil-works/pi-ai": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
40
43
|
"@earendil-works/pi-coding-agent": {
|
|
41
44
|
"optional": true
|
|
42
45
|
},
|
|
@@ -48,6 +51,6 @@
|
|
|
48
51
|
"extensions": [
|
|
49
52
|
"./extensions/status-footer.ts"
|
|
50
53
|
],
|
|
51
|
-
"image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.
|
|
54
|
+
"image": "https://cdn.jsdelivr.net/npm/pi-bar@0.3.7/assets/screenshot.png"
|
|
52
55
|
}
|
|
53
56
|
}
|