@narumitw/pi-statusline 0.7.0 → 0.9.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
@@ -14,7 +14,7 @@ Use it to monitor model selection, thinking level, git branch, working directory
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
- - Adapts to terminal width and truncates safely.
17
+ - Adapts to terminal width and wraps long extension status lines safely.
18
18
  - Requires no configuration, with optional preset selection through `PI_STATUSLINE_PRESET`.
19
19
 
20
20
  ## 📦 Install
@@ -59,6 +59,7 @@ Extension statuses use built-in icons by status key. Override or suppress them i
59
59
  {
60
60
  "extensionStatusIcons": {
61
61
  "caffeinate": "☕",
62
+ "github-pr": "🔎",
62
63
  "goal": "🎯",
63
64
  "pisync": "☁️",
64
65
  "unknown-error-retry": "",
@@ -90,13 +91,14 @@ The default `tokyo-night` statusline uses a Starship-inspired `░▒▓` / `
90
91
  - 💸 estimated cost.
91
92
  - 🕒 clock.
92
93
 
93
- Statuses from other extensions appear on their own compact line below the main statusline and use each preset's separator.
94
+ Statuses from other extensions appear below the main statusline, use each preset's separator, and wrap onto additional footer lines when they exceed the terminal width.
94
95
 
95
96
  `pi-statusline` is extension-agnostic: it consumes Pi's generic extension status API and does not import or depend on status-producing extensions.
96
97
 
97
98
  Examples:
98
99
 
99
100
  - `🎯 active` for `goal: active` using the built-in `goal` icon.
101
+ - `🔎 PR #123 checks passing` for `github-pr: PR #123 checks passing` using the built-in `github-pr` icon.
100
102
  - `☕ display` when JSON config sets `"caffeinate": "☕"`.
101
103
  - `receiving` when JSON config sets `"unknown-error-retry": ""`.
102
104
  - `🔌 running` for an unknown extension status key with no configured icon.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-statusline",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "Pi extension that replaces the footer with an information-rich statusline.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/statusline.ts CHANGED
@@ -9,7 +9,7 @@ import type {
9
9
  Theme,
10
10
  ThemeColor,
11
11
  } from "@earendil-works/pi-coding-agent";
12
- import { truncateToWidth } from "@earendil-works/pi-tui";
12
+ import { truncateToWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
13
13
  import { classicExtensionSeparator, renderClassicStatusline } from "../presets/classic.js";
14
14
  import { renderTokyoNightStatusline, tokyoNightExtensionSeparator } from "../presets/tokyo-night.js";
15
15
  import type {
@@ -41,6 +41,7 @@ interface TokenTotals {
41
41
  }
42
42
 
43
43
  const STATUSLINE_KEY = "statusline";
44
+ const GITHUB_PR_KEY = "github-pr";
44
45
  const SETTINGS_FILE = "pi-statusline-settings.json";
45
46
  const DEFAULT_PRESET: StatuslinePresetName = "tokyo-night";
46
47
 
@@ -49,6 +50,7 @@ const DEFAULT_EXTENSION_STATUS_ICONS: Record<string, string> = {
49
50
  "codex-usage": "📊",
50
51
  caffeinate: "💊",
51
52
  firecrawl: "🔥",
53
+ "github-pr": "🔎",
52
54
  goal: "🎯",
53
55
  lsp: "🧰",
54
56
  "plan-mode": "📝",
@@ -112,14 +114,7 @@ export default function statusline(pi: ExtensionAPI) {
112
114
  invalidate() {},
113
115
  render(width: number): string[] {
114
116
  const lines = [renderStatusline(width, ctx, footerData, theme, config, runtime)];
115
- const extensionStatusLine = renderExtensionStatusline(
116
- width,
117
- footerData,
118
- theme,
119
- config,
120
- runtime,
121
- );
122
- if (extensionStatusLine) lines.push(extensionStatusLine);
117
+ lines.push(...renderExtensionStatusline(width, footerData, theme, config, runtime));
123
118
  return lines;
124
119
  },
125
120
  };
@@ -255,11 +250,9 @@ function renderExtensionStatusline(
255
250
  theme: Theme,
256
251
  config: StatuslineConfig,
257
252
  runtime: RuntimeState,
258
- ): string | undefined {
253
+ ): string[] {
259
254
  const status = formatExtensionStatuses(footerData.getExtensionStatuses(), theme, config, runtime);
260
- if (!status) return undefined;
261
-
262
- return truncateToWidth(status, width, "");
255
+ return wrapExtensionStatusline(status, width);
263
256
  }
264
257
 
265
258
  function buildSegment(
@@ -284,8 +277,11 @@ function buildSegment(
284
277
  thinkingColor(runtime.thinkingLevel),
285
278
  "header",
286
279
  );
287
- case "branch":
288
- return segment(name, `🌿 ${footerData.getGitBranch() ?? "no-git"}`, color, "git");
280
+ case "branch": {
281
+ const branch = footerData.getGitBranch();
282
+ const pr = branch ? prLinkFromStatuses(footerData.getExtensionStatuses()) : undefined;
283
+ return segment(name, pr ? `🌿 ${branch} (${pr})` : `🌿 ${branch ?? "no-git"}`, color, "git");
284
+ }
289
285
  case "cwd":
290
286
  return segment(name, `📁 ${basename(ctx.cwd) || ctx.cwd}`, color, "directory");
291
287
  case "tools":
@@ -381,6 +377,19 @@ export function formatToolActivity(runtime: RuntimeState): string {
381
377
  return "💤 idle";
382
378
  }
383
379
 
380
+ export function prLinkFromStatuses(statuses: ReadonlyMap<string, string>): string | undefined {
381
+ const value = statuses.get(GITHUB_PR_KEY);
382
+ if (!value) return undefined;
383
+ // Extract the OSC 8 hyperlink span (the clickable "#123"); skip non-PR states
384
+ // like "PR gh missing" that carry no link. github-pr emits exactly one link, so the
385
+ // first OSC 8 span is the PR number.
386
+ const open = value.indexOf("\x1b]8;;");
387
+ if (open === -1) return undefined;
388
+ const closeMarker = "\x1b]8;;\x07";
389
+ const close = value.indexOf(closeMarker, open + 1);
390
+ return close === -1 ? undefined : value.slice(open, close + closeMarker.length);
391
+ }
392
+
384
393
  function formatExtensionStatuses(
385
394
  statuses: ReadonlyMap<string, string>,
386
395
  theme: Theme,
@@ -391,7 +400,11 @@ function formatExtensionStatuses(
391
400
  const visibleStatuses = [
392
401
  ...formatDuplicateExtensionStatus(runtime, theme),
393
402
  ...[...statuses.entries()]
394
- .filter(([key, value]) => key !== STATUSLINE_KEY && value.trim().length > 0)
403
+ // github-pr is rendered inline in the branch segment, so skip it here to avoid duplication.
404
+ .filter(
405
+ ([key, value]) =>
406
+ key !== STATUSLINE_KEY && key !== GITHUB_PR_KEY && value.trim().length > 0,
407
+ )
395
408
  .map(([key, value]) => formatExtensionStatus(key, value, theme, config)),
396
409
  ].slice(0, 5);
397
410
 
@@ -405,7 +418,7 @@ export function formatExtensionStatus(
405
418
  config: Pick<StatuslineConfig, "extensionStatusIcons">,
406
419
  ): string {
407
420
  const status = splitExtensionStatusIcon(stripExtensionStatusPrefix(key, value));
408
- const text = truncateToWidth(simplifyExtensionStatusText(status.text), 22, "…");
421
+ const text = simplifyExtensionStatusText(status.text);
409
422
  const color = extensionColor(key, value);
410
423
  const textColor = color === "warning" ? "warning" : "muted";
411
424
  const icon = extensionStatusIcon(key, status.icon, config.extensionStatusIcons);
@@ -422,6 +435,11 @@ function extensionStatusIcon(
422
435
  return leadingIcon ?? DEFAULT_EXTENSION_STATUS_ICONS[key] ?? "🔌";
423
436
  }
424
437
 
438
+ export function wrapExtensionStatusline(status: string, width: number): string[] {
439
+ if (!status || width <= 0) return [];
440
+ return wrapTextWithAnsi(status, width);
441
+ }
442
+
425
443
  function formatDuplicateExtensionStatus(runtime: RuntimeState, theme: Theme): string[] {
426
444
  if (runtime.duplicateExtensions.length === 0) return [];
427
445
  const names = runtime.duplicateExtensions.slice(0, 2).join(", ");