@narumitw/pi-statusline 0.5.0 → 0.6.1
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 -7
- package/package.json +1 -1
- package/presets/types.ts +1 -0
- package/src/statusline.ts +62 -5
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Use it to monitor model selection, thinking level, git branch, working directory
|
|
|
11
11
|
- Replaces the default Pi footer with a compact preset-based statusline.
|
|
12
12
|
- Shows model, thinking level, git branch, project directory, active tool, context usage, tokens, cost, and clock.
|
|
13
13
|
- Displays compact statuses published through Pi's generic extension status API.
|
|
14
|
-
-
|
|
14
|
+
- Owns extension status icons through optional JSON config, including per-extension icon suppression with `""`.
|
|
15
15
|
- Warns when the same extension package is installed from multiple sources.
|
|
16
16
|
- Uses emoji-labeled segments for readability in both classic and Tokyo Night presets.
|
|
17
17
|
- Adapts to terminal width and truncates safely.
|
|
@@ -49,7 +49,31 @@ Supported presets:
|
|
|
49
49
|
- `tokyo-night` — the default, inspired by the [Starship Tokyo Night preset](https://starship.rs/presets/tokyo-night), using `░▒▓` / `` powerline blocks and the Tokyo Night color ramp.
|
|
50
50
|
- `classic` — a compact Pi-themed statusline with left-aligned `•` separators.
|
|
51
51
|
|
|
52
|
-
Unset or invalid values fall back to `tokyo-night`. Both presets keep the same emoji-labeled information
|
|
52
|
+
Unset or invalid values fall back to `tokyo-night`. Both presets keep the same emoji-labeled information.
|
|
53
|
+
|
|
54
|
+
## ⚙️ Extension status icons
|
|
55
|
+
|
|
56
|
+
Extension statuses use built-in icons by status key. Override or suppress them in `${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-statusline-settings.json`:
|
|
57
|
+
|
|
58
|
+
```json
|
|
59
|
+
{
|
|
60
|
+
"extensionStatusIcons": {
|
|
61
|
+
"caffeinate": "☕",
|
|
62
|
+
"goal": "🎯",
|
|
63
|
+
"pisync": "☁️",
|
|
64
|
+
"unknown-error-retry": "",
|
|
65
|
+
"plan-mode": "📝",
|
|
66
|
+
"subagents": "🤖"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- Missing key: use the built-in icon, or `🔌` for an unknown status key.
|
|
72
|
+
- String value: use that string as the icon.
|
|
73
|
+
- Empty string: show the status text without an icon.
|
|
74
|
+
- `PI_STATUSLINE_PRESET` remains the only preset setting; this JSON file only controls extension status icons.
|
|
75
|
+
|
|
76
|
+
During the `PI_CAFFEINATE_ICON` deprecation window, a leading emoji from `pi-caffeinate` is still used when JSON does not configure `caffeinate`. JSON wins when both are set.
|
|
53
77
|
|
|
54
78
|
## 👀 What it shows
|
|
55
79
|
|
|
@@ -68,14 +92,14 @@ The default `tokyo-night` statusline uses a Starship-inspired `░▒▓` / `
|
|
|
68
92
|
|
|
69
93
|
Statuses from other extensions appear on their own compact line below the main statusline and use each preset's separator.
|
|
70
94
|
|
|
71
|
-
`pi-statusline` is extension-agnostic: it consumes Pi's generic extension status API and does not import or depend on status-producing extensions.
|
|
95
|
+
`pi-statusline` is extension-agnostic: it consumes Pi's generic extension status API and does not import or depend on status-producing extensions.
|
|
72
96
|
|
|
73
97
|
Examples:
|
|
74
98
|
|
|
75
|
-
-
|
|
76
|
-
-
|
|
77
|
-
-
|
|
78
|
-
-
|
|
99
|
+
- `🎯 active` for `goal: active` using the built-in `goal` icon.
|
|
100
|
+
- `☕ display` when JSON config sets `"caffeinate": "☕"`.
|
|
101
|
+
- `receiving` when JSON config sets `"unknown-error-retry": ""`.
|
|
102
|
+
- `🔌 running` for an unknown extension status key with no configured icon.
|
|
79
103
|
- `⚠️ dup biome-lsp` when local and npm installs register the same extension.
|
|
80
104
|
|
|
81
105
|
## 🧠 Use cases
|
package/package.json
CHANGED
package/presets/types.ts
CHANGED
package/src/statusline.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import process from "node:process";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import type {
|
|
5
6
|
ExtensionAPI,
|
|
6
7
|
ExtensionContext,
|
|
@@ -40,8 +41,26 @@ interface TokenTotals {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
const STATUSLINE_KEY = "statusline";
|
|
44
|
+
const SETTINGS_FILE = "pi-statusline-settings.json";
|
|
43
45
|
const DEFAULT_PRESET: StatuslinePresetName = "tokyo-night";
|
|
44
46
|
|
|
47
|
+
const DEFAULT_EXTENSION_STATUS_ICONS: Record<string, string> = {
|
|
48
|
+
"chrome-devtools": "🌐",
|
|
49
|
+
"codex-usage": "📊",
|
|
50
|
+
caffeinate: "💊",
|
|
51
|
+
firecrawl: "🔥",
|
|
52
|
+
goal: "🎯",
|
|
53
|
+
lsp: "🧰",
|
|
54
|
+
"plan-mode": "📝",
|
|
55
|
+
pisync: "🔄",
|
|
56
|
+
subagents: "🧑🤝🧑",
|
|
57
|
+
"unknown-error-retry": "🔁",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
interface StatuslineSettings {
|
|
61
|
+
extensionStatusIcons: Record<string, string>;
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
const DEFAULT_SEGMENTS: SegmentName[] = [
|
|
46
65
|
"brand",
|
|
47
66
|
"model",
|
|
@@ -173,6 +192,28 @@ function createDefaultConfig(): StatuslineConfig {
|
|
|
173
192
|
separator: "dot",
|
|
174
193
|
showLabels: false,
|
|
175
194
|
segments: [...DEFAULT_SEGMENTS],
|
|
195
|
+
extensionStatusIcons: readStatuslineSettings().extensionStatusIcons,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function readStatuslineSettings(settingsPath = join(getAgentDir(), SETTINGS_FILE)): StatuslineSettings {
|
|
200
|
+
try {
|
|
201
|
+
return normalizeStatuslineSettings(JSON.parse(readFileSync(settingsPath, "utf8")));
|
|
202
|
+
} catch {
|
|
203
|
+
return { extensionStatusIcons: {} };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function normalizeStatuslineSettings(value: unknown): StatuslineSettings {
|
|
208
|
+
if (!value || typeof value !== "object") return { extensionStatusIcons: {} };
|
|
209
|
+
const icons = (value as { extensionStatusIcons?: unknown }).extensionStatusIcons;
|
|
210
|
+
if (!icons || typeof icons !== "object" || Array.isArray(icons)) {
|
|
211
|
+
return { extensionStatusIcons: {} };
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
extensionStatusIcons: Object.fromEntries(
|
|
215
|
+
Object.entries(icons).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
|
|
216
|
+
),
|
|
176
217
|
};
|
|
177
218
|
}
|
|
178
219
|
|
|
@@ -351,18 +392,34 @@ function formatExtensionStatuses(
|
|
|
351
392
|
...formatDuplicateExtensionStatus(runtime, theme),
|
|
352
393
|
...[...statuses.entries()]
|
|
353
394
|
.filter(([key, value]) => key !== STATUSLINE_KEY && value.trim().length > 0)
|
|
354
|
-
.map(([key, value]) => formatExtensionStatus(key, value, theme)),
|
|
395
|
+
.map(([key, value]) => formatExtensionStatus(key, value, theme, config)),
|
|
355
396
|
].slice(0, 5);
|
|
356
397
|
|
|
357
398
|
return visibleStatuses.join(separator);
|
|
358
399
|
}
|
|
359
400
|
|
|
360
|
-
function formatExtensionStatus(
|
|
401
|
+
export function formatExtensionStatus(
|
|
402
|
+
key: string,
|
|
403
|
+
value: string,
|
|
404
|
+
theme: Theme,
|
|
405
|
+
config: Pick<StatuslineConfig, "extensionStatusIcons">,
|
|
406
|
+
): string {
|
|
361
407
|
const status = splitExtensionStatusIcon(stripExtensionStatusPrefix(key, value));
|
|
362
408
|
const text = truncateToWidth(simplifyExtensionStatusText(status.text), 22, "…");
|
|
363
409
|
const color = extensionColor(key, value);
|
|
364
410
|
const textColor = color === "warning" ? "warning" : "muted";
|
|
365
|
-
|
|
411
|
+
const icon = extensionStatusIcon(key, status.icon, config.extensionStatusIcons);
|
|
412
|
+
const renderedText = theme.fg(textColor, text);
|
|
413
|
+
return icon ? `${theme.fg(color, icon)} ${renderedText}` : renderedText;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function extensionStatusIcon(
|
|
417
|
+
key: string,
|
|
418
|
+
leadingIcon: string | undefined,
|
|
419
|
+
configuredIcons: Record<string, string>,
|
|
420
|
+
) {
|
|
421
|
+
if (Object.hasOwn(configuredIcons, key)) return configuredIcons[key];
|
|
422
|
+
return leadingIcon ?? DEFAULT_EXTENSION_STATUS_ICONS[key] ?? "🔌";
|
|
366
423
|
}
|
|
367
424
|
|
|
368
425
|
function formatDuplicateExtensionStatus(runtime: RuntimeState, theme: Theme): string[] {
|
|
@@ -373,13 +430,13 @@ function formatDuplicateExtensionStatus(runtime: RuntimeState, theme: Theme): st
|
|
|
373
430
|
return [`${theme.fg("warning", "⚠️")} ${theme.fg("warning", `dup ${names}${suffix}`)}`];
|
|
374
431
|
}
|
|
375
432
|
|
|
376
|
-
export function splitExtensionStatusIcon(value: string): { icon
|
|
433
|
+
export function splitExtensionStatusIcon(value: string): { icon?: string; text: string } {
|
|
377
434
|
const trimmed = value.trim();
|
|
378
435
|
const [firstToken, ...restTokens] = trimmed.split(/\s+/);
|
|
379
436
|
if (firstToken && isEmojiOnlyToken(firstToken)) {
|
|
380
437
|
return { icon: firstToken, text: restTokens.join(" ") };
|
|
381
438
|
}
|
|
382
|
-
return {
|
|
439
|
+
return { text: trimmed };
|
|
383
440
|
}
|
|
384
441
|
|
|
385
442
|
function isEmojiOnlyToken(value: string): boolean {
|