@narumitw/pi-statusline 0.20.0 → 0.23.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,85 +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);
99
+ case "provider":
100
+ return segment(name, ctx.model?.provider ?? "no-provider", config, "accent", "header");
103
101
  case "model":
104
- return segment(name, `🤖 ${shortenModel(ctx.model?.id ?? "no-model")}`, color, "header");
102
+ return segment(name, shortenModel(ctx.model?.id ?? "no-model"), config, "accent", "header");
105
103
  case "thinking":
106
104
  return segment(
107
105
  name,
108
- `🧠 ${runtime.thinkingLevel}`,
106
+ runtime.thinkingLevel,
107
+ config,
109
108
  thinkingColor(runtime.thinkingLevel),
110
109
  "header",
111
110
  );
112
111
  case "branch": {
113
112
  const branch = footerData.getGitBranch();
114
- const pr = branch ? prLinkFromStatuses(footerData.getExtensionStatuses()) : undefined;
115
- 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
+ );
116
121
  }
117
122
  case "cwd":
118
- return segment(name, `📁 ${basename(ctx.cwd) || ctx.cwd}`, color, "directory");
123
+ return segment(name, basename(ctx.cwd) || ctx.cwd, config, "accent", "directory");
119
124
  case "tools":
120
- return segment(name, formatToolActivity(runtime), color, "runtime");
125
+ return segment(name, formatToolActivity(runtime), config, "accent", "runtime");
121
126
  case "context": {
122
127
  const usage = ctx.getContextUsage();
123
128
  const value =
124
129
  usage?.percent === null || usage?.percent === undefined
125
- ? "🪟 ctx ?"
126
- : `🪟 ctx ${usage.percent.toFixed(0)}%`;
127
- return segment(name, value, contextColor(usage?.percent), "runtime");
130
+ ? "?"
131
+ : `${usage.percent.toFixed(0)}%`;
132
+ return segment(name, value, config, contextColor(usage?.percent), "runtime");
128
133
  }
129
134
  case "tokens": {
130
135
  const totals = getTokenTotals(ctx);
131
- if (totals.input === 0 && totals.output === 0)
132
- return segment(name, "🔢 tok 0", color, "runtime");
133
- return segment(
134
- name,
135
- `🔢 ↑${formatCount(totals.input)} ↓${formatCount(totals.output)}`,
136
- color,
137
- "runtime",
138
- );
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");
139
141
  }
140
142
  case "cost": {
141
143
  const totals = getTokenTotals(ctx);
142
- 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
+ );
143
151
  }
144
152
  case "time":
145
- return segment(name, `🕒 ${formatTime()}`, color, "meter");
153
+ return segment(name, formatTime(), config, "accent", "meter");
146
154
  case "turn":
147
- return segment(name, `🔁 #${runtime.turnCount}`, color, "meter");
155
+ return segment(name, `${runtime.turnCount}`, config, "accent", "meter");
148
156
  }
149
157
  }
150
158
 
151
159
  function segment(
152
160
  name: SegmentName,
153
- text: string,
154
- color: ThemeColor,
161
+ value: string,
162
+ config: StatuslineConfig,
163
+ color: RenderSegment["color"],
155
164
  block: TokyoNightBlockName,
156
165
  emphasis = false,
157
166
  ): RenderSegment {
158
- return { name, text, color, block, emphasis };
167
+ return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
159
168
  }
160
169
 
161
- function pickColor(config: StatuslineConfig, index: number): ThemeColor {
162
- const palette = PALETTES[config.palette];
163
- 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}`;
164
177
  }
165
178
 
166
179
  function thinkingColor(level: ThinkingLevel): ThemeColor {
@@ -217,6 +230,33 @@ export function prLinkFromStatuses(statuses: ReadonlyMap<string, string>): strin
217
230
  return close === -1 ? undefined : value.slice(open, close + closeMarker.length);
218
231
  }
219
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
+
220
260
  function getTokenTotals(ctx: ExtensionContext): TokenTotals {
221
261
  const totals: TokenTotals = { input: 0, output: 0, cost: 0 };
222
262