@narumitw/pi-statusline 0.5.0 → 0.6.0

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
@@ -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
- - Preserves extension-provided status icons when the status text starts with one.
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 and preserve extension-provided status icons.
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. If an extension wants a custom icon, it should include that icon at the start of its status text, for example `ctx.ui.setStatus("goal", "🎯 active")`. Statuses without a leading icon use the generic `🔌` icon.
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
- - `🔌 active` for a plain status such as `goal: active`.
76
- - `🎯 active` when the producing extension sets `🎯 active`.
77
- - `🐍 ty ✓ ruff ✓` when the producing extension sets a Python status with a leading icon.
78
- - `🧪 running` when any extension publishes an activity status with its own icon.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-statusline",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Pi extension that replaces the footer with an information-rich statusline.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/presets/types.ts CHANGED
@@ -26,6 +26,7 @@ export interface StatuslineConfig {
26
26
  separator: SeparatorName;
27
27
  showLabels: boolean;
28
28
  segments: SegmentName[];
29
+ extensionStatusIcons: Record<string, string>;
29
30
  }
30
31
 
31
32
  export interface RenderSegment {
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(key: string, value: string, theme: Theme): string {
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
- return `${theme.fg(color, status.icon)} ${theme.fg(textColor, text)}`;
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: string; text: string } {
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 { icon: "🔌", text: trimmed };
439
+ return { text: trimmed };
383
440
  }
384
441
 
385
442
  function isEmojiOnlyToken(value: string): boolean {