pi-bar 0.3.5 → 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 +31 -10
- package/extensions/status-footer.ts +612 -7
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -3,22 +3,23 @@
|
|
|
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 puts the active model, thinking level, context usage, and extension statuses in pi's footer so you catch wrong-model / wrong-reasoning / context-pressure problems before they cost you.
|
|
6
|
+
**Never accidentally run Opus on a typo again.** pi-bar puts the active model, thinking level, context usage, live TLDR, and extension statuses in pi's footer so you catch wrong-model / wrong-reasoning / context-pressure problems before they cost you.
|
|
7
7
|
|
|
8
8
|
```text
|
|
9
|
-
◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M ❯ ✧ plan:active
|
|
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 live TLDR inline** — pi-bar can generate a compact live summary directly in the footer.
|
|
19
20
|
- **Keep extension statuses visible** — statuses set with `ctx.ui.setStatus()` still appear in the custom footer.
|
|
20
21
|
|
|
21
|
-
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 / extension-status line that refreshes when model, thinking level, context usage, or extension statuses change.
|
|
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.
|
|
22
23
|
|
|
23
24
|
## Install
|
|
24
25
|
|
|
@@ -39,11 +40,29 @@ pi-bar works out of the box, but you can tune it with environment variables befo
|
|
|
39
40
|
### Show or hide segments
|
|
40
41
|
|
|
41
42
|
```bash
|
|
42
|
-
PI_BAR_SHOW=model,thinking,context,extensions pi
|
|
43
|
+
PI_BAR_SHOW=model,thinking,context,tldr,extensions pi
|
|
43
44
|
PI_BAR_SHOW=model,context pi
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
Allowed segments are `model`, `thinking`, `context`, and `extensions`. The `extensions` segment is hidden when no extension has set a status.
|
|
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
|
+
|
|
49
|
+
### Configure live TLDR
|
|
50
|
+
|
|
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
|
+
|
|
53
|
+
```bash
|
|
54
|
+
PI_BAR_TLDR_MAX_WIDTH=48 pi
|
|
55
|
+
```
|
|
56
|
+
|
|
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:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
PI_BAR_TLDR_MODEL=anthropic/claude-haiku-4-5 pi
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
You can also set `bar.tldrModel` in pi settings; `tldr.model` is accepted for compatibility with existing settings.
|
|
47
66
|
|
|
48
67
|
### Configure extension statuses
|
|
49
68
|
|
|
@@ -76,10 +95,10 @@ PI_BAR_ICONS=nerdfont pi
|
|
|
76
95
|
|
|
77
96
|
Supported presets:
|
|
78
97
|
|
|
79
|
-
- `unicode` — default, e.g. `◈ gpt-5.5 ❯ ✦ think:medium ❯ ◷ 42.7% / 272k ❯ ✧ plan:active`
|
|
80
|
-
- `plain` — maximum compatibility, e.g. `model:gpt-5.5 | think:medium | ctx:42.7% / 272k | ext:plan:active`
|
|
81
|
-
- `emoji` — colorful emoji markers, e.g. `🤖 gpt-5.5 › ✨ think:medium › 🧭 42.7% / 272k › 🧩 plan:active`
|
|
82
|
-
- `nerdfont` — icon-font glyphs for terminals configured with a Nerd Font, e.g. ` gpt-5.5 think:medium 42.7% / 272k plan:active`
|
|
98
|
+
- `unicode` — default, e.g. `◈ gpt-5.5 ❯ ✦ think:medium ❯ ◷ 42.7% / 272k ❯ ⓘ Reading files ❯ ✧ plan:active`
|
|
99
|
+
- `plain` — maximum compatibility, e.g. `model:gpt-5.5 | think:medium | ctx:42.7% / 272k | tldr:Reading files | ext:plan:active`
|
|
100
|
+
- `emoji` — colorful emoji markers, e.g. `🤖 gpt-5.5 › ✨ think:medium › 🧭 42.7% / 272k › 📝 Reading files › 🧩 plan:active`
|
|
101
|
+
- `nerdfont` — icon-font glyphs for terminals configured with a Nerd Font, e.g. ` gpt-5.5 think:medium 42.7% / 272k Reading files plan:active`
|
|
83
102
|
|
|
84
103
|
## Compatibility
|
|
85
104
|
|
|
@@ -97,3 +116,5 @@ PI_BAR_ICONS=plain pi
|
|
|
97
116
|
## Security note
|
|
98
117
|
|
|
99
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.
|
|
@@ -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> ❯ ✧ <extension statuses>
|
|
5
|
+
* ◈ <model name> ❯ ✦ think:<level> ❯ ◷ <context% / window> ❯ ⓘ <tldr> ❯ ✧ <extension statuses>
|
|
6
6
|
*
|
|
7
7
|
* Examples:
|
|
8
8
|
* ◈ claude-opus-4.7 ❯ ✦ think:med ❯ ◷ 2.6% / 1.0M
|
|
@@ -11,18 +11,22 @@
|
|
|
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,extensions
|
|
14
|
+
* PI_BAR_SHOW=model,thinking,context,tldr,extensions
|
|
15
15
|
* PI_BAR_THRESHOLDS=70,90
|
|
16
16
|
* PI_BAR_ICONS=unicode|plain|emoji|nerdfont
|
|
17
|
+
* PI_BAR_TLDR_MAX_WIDTH=64
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
20
21
|
import { homedir } from "node:os";
|
|
21
22
|
import { dirname, join } from "node:path";
|
|
23
|
+
import { complete, type UserMessage } from "@earendil-works/pi-ai";
|
|
22
24
|
import {
|
|
23
25
|
getSettingsListTheme,
|
|
26
|
+
SettingsManager,
|
|
24
27
|
type ExtensionAPI,
|
|
25
28
|
type ExtensionContext,
|
|
29
|
+
type ThemeColor,
|
|
26
30
|
} from "@earendil-works/pi-coding-agent";
|
|
27
31
|
import {
|
|
28
32
|
Container,
|
|
@@ -31,7 +35,7 @@ import {
|
|
|
31
35
|
truncateToWidth,
|
|
32
36
|
} from "@earendil-works/pi-tui";
|
|
33
37
|
|
|
34
|
-
type SegmentName = "model" | "thinking" | "context" | "extensions";
|
|
38
|
+
type SegmentName = "model" | "thinking" | "context" | "tldr" | "extensions";
|
|
35
39
|
type IconPreset = "unicode" | "plain" | "emoji" | "nerdfont";
|
|
36
40
|
type StatusFilter =
|
|
37
41
|
| { mode: "all"; hidden: Set<string> }
|
|
@@ -39,8 +43,48 @@ type StatusFilter =
|
|
|
39
43
|
type SerializedStatusFilter =
|
|
40
44
|
| { mode: "all"; hidden: string[] }
|
|
41
45
|
| { mode: "only"; shown: string[] };
|
|
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>;
|
|
75
|
+
};
|
|
42
76
|
|
|
43
77
|
const STATUS_FILTER_ENTRY_TYPE = "pi-bar-status-filter";
|
|
78
|
+
const DEFAULT_TLDR_MAX_WIDTH = 64;
|
|
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;
|
|
44
88
|
const CONFIG_PATH =
|
|
45
89
|
process.env.PI_BAR_CONFIG ?? join(homedir(), ".pi", "agent", "pi-bar.json");
|
|
46
90
|
|
|
@@ -48,6 +92,7 @@ const DEFAULT_SEGMENTS: SegmentName[] = [
|
|
|
48
92
|
"model",
|
|
49
93
|
"thinking",
|
|
50
94
|
"context",
|
|
95
|
+
"tldr",
|
|
51
96
|
"extensions",
|
|
52
97
|
];
|
|
53
98
|
const DEFAULT_WARNING_THRESHOLD = 70;
|
|
@@ -59,6 +104,7 @@ const ICON_PRESETS: Record<
|
|
|
59
104
|
model: string;
|
|
60
105
|
thinking: string;
|
|
61
106
|
context: string;
|
|
107
|
+
tldr: string;
|
|
62
108
|
extensions: string;
|
|
63
109
|
separator: string;
|
|
64
110
|
}
|
|
@@ -67,6 +113,7 @@ const ICON_PRESETS: Record<
|
|
|
67
113
|
model: "◈",
|
|
68
114
|
thinking: "✦",
|
|
69
115
|
context: "◷",
|
|
116
|
+
tldr: "ⓘ",
|
|
70
117
|
extensions: "✧",
|
|
71
118
|
separator: "❯",
|
|
72
119
|
},
|
|
@@ -74,6 +121,7 @@ const ICON_PRESETS: Record<
|
|
|
74
121
|
model: "model",
|
|
75
122
|
thinking: "think",
|
|
76
123
|
context: "ctx",
|
|
124
|
+
tldr: "tldr",
|
|
77
125
|
extensions: "ext",
|
|
78
126
|
separator: "|",
|
|
79
127
|
},
|
|
@@ -81,6 +129,7 @@ const ICON_PRESETS: Record<
|
|
|
81
129
|
model: "🤖",
|
|
82
130
|
thinking: "✨",
|
|
83
131
|
context: "🧭",
|
|
132
|
+
tldr: "📝",
|
|
84
133
|
extensions: "🧩",
|
|
85
134
|
separator: "›",
|
|
86
135
|
},
|
|
@@ -88,6 +137,7 @@ const ICON_PRESETS: Record<
|
|
|
88
137
|
model: "",
|
|
89
138
|
thinking: "",
|
|
90
139
|
context: "",
|
|
140
|
+
tldr: "",
|
|
91
141
|
extensions: "",
|
|
92
142
|
separator: "",
|
|
93
143
|
},
|
|
@@ -111,7 +161,7 @@ function formatModelName(id: string | undefined): string {
|
|
|
111
161
|
return base.replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
112
162
|
}
|
|
113
163
|
|
|
114
|
-
function thinkingColor(level: string):
|
|
164
|
+
function thinkingColor(level: string): ThemeColor {
|
|
115
165
|
switch (level) {
|
|
116
166
|
case "off":
|
|
117
167
|
return "thinkingOff";
|
|
@@ -137,7 +187,7 @@ function contextColor(
|
|
|
137
187
|
percent: number | null | undefined,
|
|
138
188
|
warningThreshold: number,
|
|
139
189
|
errorThreshold: number,
|
|
140
|
-
):
|
|
190
|
+
): ThemeColor {
|
|
141
191
|
if (percent === null || percent === undefined) return "muted";
|
|
142
192
|
if (percent >= errorThreshold) return "error";
|
|
143
193
|
if (percent >= warningThreshold) return "warning";
|
|
@@ -152,7 +202,7 @@ function parseSegments(): SegmentName[] {
|
|
|
152
202
|
.split(",")
|
|
153
203
|
.map((segment) => segment.trim().toLowerCase())
|
|
154
204
|
.filter((segment): segment is SegmentName =>
|
|
155
|
-
["model", "thinking", "context", "extensions"].includes(segment),
|
|
205
|
+
["model", "thinking", "context", "tldr", "extensions"].includes(segment),
|
|
156
206
|
);
|
|
157
207
|
|
|
158
208
|
return requested.length > 0 ? requested : DEFAULT_SEGMENTS;
|
|
@@ -192,11 +242,543 @@ function parseIconPreset(): IconPreset {
|
|
|
192
242
|
return "unicode";
|
|
193
243
|
}
|
|
194
244
|
|
|
245
|
+
function parseTldrMaxWidth(): number {
|
|
246
|
+
const raw = process.env.PI_BAR_TLDR_MAX_WIDTH;
|
|
247
|
+
if (!raw) return DEFAULT_TLDR_MAX_WIDTH;
|
|
248
|
+
|
|
249
|
+
const value = Number.parseInt(raw, 10);
|
|
250
|
+
if (Number.isFinite(value) && value >= 10 && value <= 500) return value;
|
|
251
|
+
return DEFAULT_TLDR_MAX_WIDTH;
|
|
252
|
+
}
|
|
253
|
+
|
|
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) };
|
|
260
|
+
}
|
|
261
|
+
|
|
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
|
+
}
|
|
274
|
+
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function resolveTldrModelPreference(cwd: string): TldrModelPreference | undefined {
|
|
279
|
+
const envModel = process.env.PI_BAR_TLDR_MODEL;
|
|
280
|
+
if (envModel) return parseTldrModelSpec(envModel);
|
|
281
|
+
|
|
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;
|
|
330
|
+
}
|
|
331
|
+
const auth = await getModelAuth(ctx, candidate);
|
|
332
|
+
if (auth) return auth;
|
|
333
|
+
}
|
|
334
|
+
|
|
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;
|
|
356
|
+
}
|
|
357
|
+
|
|
195
358
|
function prefixed(icon: string, text: string, preset: IconPreset): string {
|
|
196
359
|
if (preset === "plain") return `${icon}:${text}`;
|
|
197
360
|
return `${icon} ${text}`;
|
|
198
361
|
}
|
|
199
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
|
+
|
|
200
782
|
function shouldShowStatus(key: string, filter: StatusFilter): boolean {
|
|
201
783
|
if (filter.mode === "only") return filter.shown.has(key);
|
|
202
784
|
return !filter.hidden.has(key);
|
|
@@ -293,6 +875,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
293
875
|
let statusFilter: StatusFilter = { mode: "all", hidden: new Set() };
|
|
294
876
|
const seenStatusKeys = new Set<string>();
|
|
295
877
|
const refresh = () => requestRender?.();
|
|
878
|
+
const tldr = new FooterTldrEngine(refresh);
|
|
296
879
|
const restoreStatusFilter = (ctx: ExtensionContext) => {
|
|
297
880
|
let restoredFilter = readGlobalStatusFilter();
|
|
298
881
|
if (!restoredFilter) {
|
|
@@ -479,13 +1062,29 @@ export default function (pi: ExtensionAPI) {
|
|
|
479
1062
|
pi.on("model_select", async () => refresh());
|
|
480
1063
|
pi.on("thinking_level_select", async () => refresh());
|
|
481
1064
|
pi.on("turn_end", async () => refresh());
|
|
482
|
-
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
|
+
});
|
|
483
1081
|
pi.on("session_tree", async (_event, ctx) => {
|
|
484
1082
|
restoreStatusFilter(ctx);
|
|
485
1083
|
refresh();
|
|
486
1084
|
});
|
|
487
1085
|
|
|
488
1086
|
pi.on("session_start", async (_event, ctx) => {
|
|
1087
|
+
tldr.startSession(ctx.cwd);
|
|
489
1088
|
restoreStatusFilter(ctx);
|
|
490
1089
|
|
|
491
1090
|
if (!ctx.hasUI) return;
|
|
@@ -494,6 +1093,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
494
1093
|
const iconPreset = parseIconPreset();
|
|
495
1094
|
const icons = ICON_PRESETS[iconPreset];
|
|
496
1095
|
const { warningThreshold, errorThreshold } = parseThresholds();
|
|
1096
|
+
const tldrMaxWidth = parseTldrMaxWidth();
|
|
497
1097
|
|
|
498
1098
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
499
1099
|
requestRender = () => tui.requestRender();
|
|
@@ -521,6 +1121,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
521
1121
|
const contextText = usage
|
|
522
1122
|
? `${usage.percent !== null ? `${usage.percent.toFixed(1)}%` : "—%"} / ${formatTokens(usage.contextWindow)}`
|
|
523
1123
|
: "—";
|
|
1124
|
+
const tldrText = tldr.text(tldrMaxWidth);
|
|
524
1125
|
|
|
525
1126
|
const segmentRenderers: Record<SegmentName, string | null> = {
|
|
526
1127
|
model: theme.fg(
|
|
@@ -535,6 +1136,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
535
1136
|
contextSegmentColor,
|
|
536
1137
|
prefixed(icons.context, contextText, iconPreset),
|
|
537
1138
|
),
|
|
1139
|
+
tldr: tldrText
|
|
1140
|
+
? theme.fg("muted", prefixed(icons.tldr, tldrText, iconPreset))
|
|
1141
|
+
: null,
|
|
538
1142
|
extensions: extensionStatuses
|
|
539
1143
|
? theme.fg(
|
|
540
1144
|
"muted",
|
|
@@ -556,6 +1160,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
556
1160
|
});
|
|
557
1161
|
|
|
558
1162
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
1163
|
+
tldr.shutdown();
|
|
559
1164
|
if (ctx.hasUI) ctx.ui.setFooter(undefined);
|
|
560
1165
|
});
|
|
561
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, and extension statuses.",
|
|
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",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"model",
|
|
14
14
|
"thinking",
|
|
15
15
|
"context",
|
|
16
|
-
"context-window"
|
|
16
|
+
"context-window",
|
|
17
|
+
"tldr"
|
|
17
18
|
],
|
|
18
19
|
"license": "MIT",
|
|
19
20
|
"repository": {
|
|
@@ -31,10 +32,14 @@
|
|
|
31
32
|
"LICENSE"
|
|
32
33
|
],
|
|
33
34
|
"peerDependencies": {
|
|
35
|
+
"@earendil-works/pi-ai": "*",
|
|
34
36
|
"@earendil-works/pi-coding-agent": "*",
|
|
35
37
|
"@earendil-works/pi-tui": "*"
|
|
36
38
|
},
|
|
37
39
|
"peerDependenciesMeta": {
|
|
40
|
+
"@earendil-works/pi-ai": {
|
|
41
|
+
"optional": true
|
|
42
|
+
},
|
|
38
43
|
"@earendil-works/pi-coding-agent": {
|
|
39
44
|
"optional": true
|
|
40
45
|
},
|
|
@@ -46,6 +51,6 @@
|
|
|
46
51
|
"extensions": [
|
|
47
52
|
"./extensions/status-footer.ts"
|
|
48
53
|
],
|
|
49
|
-
"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"
|
|
50
55
|
}
|
|
51
56
|
}
|