@narumitw/pi-statusline 0.21.0 → 0.25.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/src/render.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { basename } from "node:path";
2
- import process from "node:process";
3
2
  import type {
4
3
  ExtensionAPI,
5
4
  ExtensionContext,
@@ -7,22 +6,21 @@ import type {
7
6
  Theme,
8
7
  ThemeColor,
9
8
  } from "@earendil-works/pi-coding-agent";
10
- import { truncateToWidth } from "@earendil-works/pi-tui";
11
- import { renderClassicStatusline } from "../presets/classic.js";
12
- import { renderTokyoNightStatusline } from "../presets/tokyo-night.js";
13
- import type {
14
- PaletteName,
15
- RenderSegment,
16
- SegmentName,
17
- StatuslineConfig,
18
- TokyoNightBlockName,
19
- } from "../presets/types.js";
20
9
  import {
21
- formatExtensionStatuses,
22
10
  type ExtensionStatusRuntime,
11
+ formatExtensionStatuses,
23
12
  wrapExtensionStatusline,
24
13
  } from "./extension-status.js";
25
- import { formatGitBranchText, type GitStatusSummary } from "./git-status.js";
14
+ import { formatGitBranchValue, type GitStatusSummary } from "./git-status.js";
15
+ import { renderTokyoNightStatusline } from "./tokyo-night.js";
16
+ import {
17
+ LINE_BREAK_SEGMENT_NAME,
18
+ type RenderItem,
19
+ type RenderSegment,
20
+ type SegmentName,
21
+ type StatuslineConfig,
22
+ type TokyoNightBlockName,
23
+ } from "./types.js";
26
24
 
27
25
  type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
28
26
  export interface RuntimeState extends ExtensionStatusRuntime {
@@ -41,39 +39,30 @@ interface TokenTotals {
41
39
  cost: number;
42
40
  }
43
41
  const GITHUB_PR_KEY = "github-pr";
44
- const PALETTES: Record<PaletteName, ThemeColor[]> = {
45
- ocean: ["accent", "muted", "success", "warning"],
46
- sunset: ["warning", "accent", "success", "muted"],
47
- forest: ["success", "accent", "muted", "warning"],
48
- candy: ["accent", "warning", "success", "muted"],
49
- neon: ["accent", "success", "warning", "error"],
50
- mono: ["muted", "dim"],
51
- };
52
-
42
+ const GITHUB_PR_STATUS_KEYS = new Set([GITHUB_PR_KEY]);
53
43
  export function renderStatusline(
54
44
  width: number,
55
45
  ctx: ExtensionContext,
56
46
  footerData: ReadonlyFooterDataProvider,
57
- theme: Theme,
47
+ _theme: Theme,
58
48
  config: StatuslineConfig,
59
49
  runtime: RuntimeState,
60
50
  ): string {
61
51
  if (width <= 0) return "";
62
52
 
63
53
  const segments = config.segments
64
- .map((segment, index) => buildSegment(segment, index, ctx, footerData, config, runtime))
54
+ .map((name): RenderItem | undefined =>
55
+ name === LINE_BREAK_SEGMENT_NAME
56
+ ? { name: LINE_BREAK_SEGMENT_NAME }
57
+ : buildSegment(name, ctx, footerData, config, runtime),
58
+ )
65
59
  .filter(
66
- (segment): segment is RenderSegment => segment !== undefined && segment.text.length > 0,
60
+ (segment): segment is RenderItem =>
61
+ segment !== undefined &&
62
+ (segment.name === LINE_BREAK_SEGMENT_NAME || segment.text.length > 0),
67
63
  );
68
64
 
69
- if (segments.length === 0) return truncateToWidth(theme.fg("dim", "pi-statusline"), width);
70
-
71
- switch (config.preset) {
72
- case "classic":
73
- return renderClassicStatusline(width, segments, theme, config);
74
- case "tokyo-night":
75
- return renderTokyoNightStatusline(width, segments);
76
- }
65
+ return renderTokyoNightStatusline(width, segments, config);
77
66
  }
78
67
 
79
68
  export function renderExtensionStatusline(
@@ -82,87 +71,109 @@ export function renderExtensionStatusline(
82
71
  theme: Theme,
83
72
  config: StatuslineConfig,
84
73
  runtime: RuntimeState,
74
+ mainLine: string,
85
75
  ): string[] {
86
- const status = formatExtensionStatuses(footerData.getExtensionStatuses(), theme, config, runtime);
76
+ const statuses = footerData.getExtensionStatuses();
77
+ const prContext = prContextFromStatuses(statuses);
78
+ const rendersPrInline = prContext !== undefined && mainLine.includes(prContext);
79
+ const status = formatExtensionStatuses(
80
+ statuses,
81
+ theme,
82
+ config,
83
+ runtime,
84
+ rendersPrInline ? GITHUB_PR_STATUS_KEYS : undefined,
85
+ );
87
86
  return wrapExtensionStatusline(status, width);
88
87
  }
89
88
 
90
89
  function buildSegment(
91
90
  name: SegmentName,
92
- index: number,
93
91
  ctx: ExtensionContext,
94
92
  footerData: ReadonlyFooterDataProvider,
95
93
  config: StatuslineConfig,
96
94
  runtime: RuntimeState,
97
95
  ): RenderSegment | undefined {
98
- const color = pickColor(config, index);
99
-
100
96
  switch (name) {
101
97
  case "brand":
102
- return segment(name, "π", "accent", "header", true);
98
+ return segment(name, "π", config, "accent", "header", true);
103
99
  case "provider":
104
- return segment(name, `🔌 ${ctx.model?.provider ?? "no-provider"}`, color, "header");
100
+ return segment(name, ctx.model?.provider ?? "no-provider", config, "accent", "header");
105
101
  case "model":
106
- return segment(name, `🤖 ${shortenModel(ctx.model?.id ?? "no-model")}`, color, "header");
102
+ return segment(name, shortenModel(ctx.model?.id ?? "no-model"), config, "accent", "header");
107
103
  case "thinking":
108
104
  return segment(
109
105
  name,
110
- `🧠 ${runtime.thinkingLevel}`,
106
+ runtime.thinkingLevel,
107
+ config,
111
108
  thinkingColor(runtime.thinkingLevel),
112
109
  "header",
113
110
  );
114
111
  case "branch": {
115
112
  const branch = footerData.getGitBranch();
116
- const pr = branch ? prLinkFromStatuses(footerData.getExtensionStatuses()) : undefined;
117
- return segment(name, formatGitBranchText(branch, runtime.gitStatus, pr), color, "git");
113
+ const pr = branch ? prContextFromStatuses(footerData.getExtensionStatuses()) : undefined;
114
+ return segment(
115
+ name,
116
+ formatGitBranchValue(branch, runtime.gitStatus, pr),
117
+ config,
118
+ "accent",
119
+ "git",
120
+ );
118
121
  }
119
122
  case "cwd":
120
- return segment(name, `📁 ${basename(ctx.cwd) || ctx.cwd}`, color, "directory");
123
+ return segment(name, basename(ctx.cwd) || ctx.cwd, config, "accent", "directory");
121
124
  case "tools":
122
- return segment(name, formatToolActivity(runtime), color, "runtime");
125
+ return segment(name, formatToolActivity(runtime), config, "accent", "runtime");
123
126
  case "context": {
124
127
  const usage = ctx.getContextUsage();
125
128
  const value =
126
129
  usage?.percent === null || usage?.percent === undefined
127
- ? "🪟 ctx ?"
128
- : `🪟 ctx ${usage.percent.toFixed(0)}%`;
129
- return segment(name, value, contextColor(usage?.percent), "runtime");
130
+ ? "?"
131
+ : `${usage.percent.toFixed(0)}%`;
132
+ return segment(name, value, config, contextColor(usage?.percent), "runtime");
130
133
  }
131
134
  case "tokens": {
132
135
  const totals = getTokenTotals(ctx);
133
- if (totals.input === 0 && totals.output === 0)
134
- return segment(name, "🔢 tok 0", color, "runtime");
135
- return segment(
136
- name,
137
- `🔢 ↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`,
138
- color,
139
- "runtime",
140
- );
136
+ const value =
137
+ totals.input === 0 && totals.output === 0
138
+ ? "tok 0"
139
+ : `↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`;
140
+ return segment(name, value, config, "accent", "runtime");
141
141
  }
142
142
  case "cost": {
143
143
  const totals = getTokenTotals(ctx);
144
- return segment(name, `💸 $${totals.cost.toFixed(totals.cost >= 1 ? 2 : 3)}`, color, "meter");
144
+ return segment(
145
+ name,
146
+ totals.cost.toFixed(totals.cost >= 1 ? 2 : 3),
147
+ config,
148
+ "accent",
149
+ "meter",
150
+ );
145
151
  }
146
152
  case "time":
147
- return segment(name, `🕒 ${formatTime()}`, color, "meter");
153
+ return segment(name, formatTime(), config, "accent", "meter");
148
154
  case "turn":
149
- return segment(name, `🔁 #${runtime.turnCount}`, color, "meter");
155
+ return segment(name, `${runtime.turnCount}`, config, "accent", "meter");
150
156
  }
151
157
  }
152
158
 
153
159
  function segment(
154
160
  name: SegmentName,
155
- text: string,
156
- color: ThemeColor,
161
+ value: string,
162
+ config: StatuslineConfig,
163
+ color: RenderSegment["color"],
157
164
  block: TokyoNightBlockName,
158
165
  emphasis = false,
159
166
  ): RenderSegment {
160
- return { name, text, color, block, emphasis };
167
+ return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
161
168
  }
162
169
 
163
- function pickColor(config: StatuslineConfig, index: number): ThemeColor {
164
- const palette = PALETTES[config.palette];
165
- return palette[index % palette.length] ?? "muted";
170
+ export function formatConfiguredSegment(
171
+ name: SegmentName,
172
+ value: string,
173
+ config: Pick<StatuslineConfig, "segmentText">,
174
+ ): string {
175
+ const presentation = config.segmentText[name];
176
+ return `${presentation.prefix}${value}${presentation.suffix}`;
166
177
  }
167
178
 
168
179
  function thinkingColor(level: ThinkingLevel): ThemeColor {
@@ -219,6 +230,33 @@ export function prLinkFromStatuses(statuses: ReadonlyMap<string, string>): strin
219
230
  return close === -1 ? undefined : value.slice(open, close + closeMarker.length);
220
231
  }
221
232
 
233
+ export function prContextFromStatuses(statuses: ReadonlyMap<string, string>): string | undefined {
234
+ const value = statuses.get(GITHUB_PR_KEY);
235
+ const link = prLinkFromStatuses(statuses);
236
+ if (!value || !link) return undefined;
237
+
238
+ const state = compactPrState(value.replace(link, ""));
239
+ return state ? `${link} · ${state}` : undefined;
240
+ }
241
+
242
+ function compactPrState(value: string): string | undefined {
243
+ if (/:\s*merged\s*$/.test(value)) return "merged";
244
+ if (/:\s*closed\s*$/.test(value)) return "closed";
245
+ if (/\bdraft\b/.test(value)) return "draft";
246
+
247
+ const failing = /\bchecks failing \((\d+)\)/.exec(value);
248
+ if (failing) return `${failing[1]} failing`;
249
+ if (/\bchanges requested\b/.test(value)) return "changes requested";
250
+
251
+ const pending = /\bchecks pending \((\d+)\)/.exec(value);
252
+ if (pending) return `${pending[1]} pending`;
253
+ if (/\bapproved\b/.test(value)) return "approved";
254
+ if (/\breview required\b/.test(value)) return "review required";
255
+ if (/\bchecks passing\b/.test(value)) return "checks passing";
256
+ if (/\bno checks\b/.test(value)) return "no checks";
257
+ return undefined;
258
+ }
259
+
222
260
  function getTokenTotals(ctx: ExtensionContext): TokenTotals {
223
261
  const totals: TokenTotals = { input: 0, output: 0, cost: 0 };
224
262