@narumitw/pi-statusline 0.50.0 → 0.50.2

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,397 +1,368 @@
1
1
  import type {
2
- ExtensionAPI,
3
- ExtensionContext,
4
- ReadonlyFooterDataProvider,
5
- Theme,
6
- ThemeColor,
7
- UIPromptKind,
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ ReadonlyFooterDataProvider,
5
+ Theme,
6
+ ThemeColor,
7
+ UIPromptKind,
8
8
  } from "@earendil-works/pi-coding-agent";
9
9
  import { sliceByColumn, visibleWidth } from "@earendil-works/pi-tui";
10
10
  import { sanitizeTerminalText } from "@narumitw/pi-tui-kit/terminal-text";
11
11
  import { formatDirectoryPath } from "./directory.js";
12
- import {
13
- type ExtensionStatusRuntime,
14
- formatExtensionStatuses,
15
- wrapExtensionStatusline,
16
- } from "./extension-status.js";
12
+ import { type ExtensionStatusRuntime, formatExtensionStatuses, wrapExtensionStatusline } from "./extension-status.js";
17
13
  import { formatGitBranchValue, type GitStatusSummary } from "./git-status.js";
18
14
  import { renderPowerlineStatusline } from "./powerline.js";
19
15
  import {
20
- LINE_BREAK_SEGMENT_NAME,
21
- type PowerlineBlockName,
22
- type RenderItem,
23
- type RenderSegment,
24
- type SegmentName,
25
- type StatuslineConfig,
26
- type TruncationDirection,
16
+ LINE_BREAK_SEGMENT_NAME,
17
+ type PowerlineBlockName,
18
+ type RenderItem,
19
+ type RenderSegment,
20
+ type SegmentName,
21
+ type StatuslineConfig,
22
+ type TruncationDirection,
27
23
  } from "./types.js";
28
24
  import { type FooterUsageSummary, summarizeFooterUsage } from "./usage.js";
29
25
 
30
26
  type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
31
27
  export interface RuntimeState extends ExtensionStatusRuntime {
32
- homeDir?: string;
33
- turnCount: number;
34
- activeTools: Map<string, number>;
35
- isStreaming: boolean;
36
- uiPrompt?: { kind: UIPromptKind; title?: string };
37
- thinkingLevel: ThinkingLevel;
38
- gitStatus?: GitStatusSummary;
39
- requestRender?: () => void;
28
+ homeDir?: string;
29
+ turnCount: number;
30
+ activeTools: Map<string, number>;
31
+ isStreaming: boolean;
32
+ uiPrompt?: { kind: UIPromptKind; title?: string };
33
+ thinkingLevel: ThinkingLevel;
34
+ gitStatus?: GitStatusSummary;
35
+ requestRender?: () => void;
40
36
  }
41
37
  const GITHUB_PR_KEY = "github-pr";
42
38
  const GITHUB_PR_STATUS_KEYS = new Set([GITHUB_PR_KEY]);
43
39
  export function renderStatusline(
44
- width: number,
45
- ctx: ExtensionContext,
46
- footerData: ReadonlyFooterDataProvider,
47
- _theme: Theme,
48
- config: StatuslineConfig,
49
- runtime: RuntimeState,
50
- trueColor = true,
40
+ width: number,
41
+ ctx: ExtensionContext,
42
+ footerData: ReadonlyFooterDataProvider,
43
+ _theme: Theme,
44
+ config: StatuslineConfig,
45
+ runtime: RuntimeState,
46
+ trueColor = true,
51
47
  ): string {
52
- if (width <= 0) return "";
48
+ if (width <= 0) return "";
53
49
 
54
- const usageSummary = summarizeFooterUsage(ctx.sessionManager.getEntries());
55
- const rows: Array<{ configuredSegments: number; segments: RenderSegment[] }> = [
56
- { configuredSegments: 0, segments: [] },
57
- ];
58
- for (const name of config.segments) {
59
- if (name === LINE_BREAK_SEGMENT_NAME) {
60
- rows.push({ configuredSegments: 0, segments: [] });
61
- continue;
62
- }
50
+ const usageSummary = summarizeFooterUsage(ctx.sessionManager.getEntries());
51
+ const rows: Array<{ configuredSegments: number; segments: RenderSegment[] }> = [
52
+ { configuredSegments: 0, segments: [] },
53
+ ];
54
+ for (const name of config.segments) {
55
+ if (name === LINE_BREAK_SEGMENT_NAME) {
56
+ rows.push({ configuredSegments: 0, segments: [] });
57
+ continue;
58
+ }
63
59
 
64
- const row = rows.at(-1);
65
- if (!row) continue;
66
- row.configuredSegments += 1;
67
- const rendered = buildSegment(name, ctx, footerData, config, runtime, usageSummary);
68
- if (rendered && rendered.text.length > 0) row.segments.push(rendered);
69
- }
60
+ const row = rows.at(-1);
61
+ if (!row) continue;
62
+ row.configuredSegments += 1;
63
+ const rendered = buildSegment(name, ctx, footerData, config, runtime, usageSummary);
64
+ if (rendered && rendered.text.length > 0) row.segments.push(rendered);
65
+ }
70
66
 
71
- const segments: RenderItem[] = [];
72
- const renderedRows = rows.filter(
73
- (row) => row.configuredSegments === 0 || row.segments.length > 0,
74
- );
75
- for (const [index, row] of renderedRows.entries()) {
76
- if (index > 0) segments.push({ name: LINE_BREAK_SEGMENT_NAME });
77
- segments.push(...row.segments);
78
- }
67
+ const segments: RenderItem[] = [];
68
+ const renderedRows = rows.filter((row) => row.configuredSegments === 0 || row.segments.length > 0);
69
+ for (const [index, row] of renderedRows.entries()) {
70
+ if (index > 0) segments.push({ name: LINE_BREAK_SEGMENT_NAME });
71
+ segments.push(...row.segments);
72
+ }
79
73
 
80
- return renderPowerlineStatusline(width, segments, config, trueColor);
74
+ return renderPowerlineStatusline(width, segments, config, trueColor);
81
75
  }
82
76
 
83
77
  export function renderExtensionStatusline(
84
- width: number,
85
- footerData: ReadonlyFooterDataProvider,
86
- theme: Theme,
87
- config: StatuslineConfig,
88
- runtime: RuntimeState,
89
- mainLine: string,
90
- trueColor = true,
78
+ width: number,
79
+ footerData: ReadonlyFooterDataProvider,
80
+ theme: Theme,
81
+ config: StatuslineConfig,
82
+ runtime: RuntimeState,
83
+ mainLine: string,
84
+ trueColor = true,
91
85
  ): string[] {
92
- const statuses = footerData.getExtensionStatuses();
93
- const prContext = prContextFromStatuses(statuses);
94
- const rendersPrInline = prContext !== undefined && mainLine.includes(prContext);
95
- const status = formatExtensionStatuses(
96
- statuses,
97
- theme,
98
- config,
99
- runtime,
100
- rendersPrInline ? GITHUB_PR_STATUS_KEYS : undefined,
101
- trueColor,
102
- );
103
- return wrapExtensionStatusline(status, width);
86
+ const statuses = footerData.getExtensionStatuses();
87
+ const prContext = prContextFromStatuses(statuses);
88
+ const rendersPrInline = prContext !== undefined && mainLine.includes(prContext);
89
+ const status = formatExtensionStatuses(
90
+ statuses,
91
+ theme,
92
+ config,
93
+ runtime,
94
+ rendersPrInline ? GITHUB_PR_STATUS_KEYS : undefined,
95
+ trueColor,
96
+ );
97
+ return wrapExtensionStatusline(status, width);
104
98
  }
105
99
 
106
100
  function buildSegment(
107
- name: SegmentName,
108
- ctx: ExtensionContext,
109
- footerData: ReadonlyFooterDataProvider,
110
- config: StatuslineConfig,
111
- runtime: RuntimeState,
112
- usageSummary: FooterUsageSummary,
101
+ name: SegmentName,
102
+ ctx: ExtensionContext,
103
+ footerData: ReadonlyFooterDataProvider,
104
+ config: StatuslineConfig,
105
+ runtime: RuntimeState,
106
+ usageSummary: FooterUsageSummary,
113
107
  ): RenderSegment | undefined {
114
- switch (name) {
115
- case "brand":
116
- return segment(name, "π", config, "accent", "header", true);
117
- case "provider":
118
- return segment(name, ctx.model?.provider ?? "no-provider", config, "accent", "header");
119
- case "model": {
120
- const presentation = config.segmentText.model;
121
- const model = truncateModel(
122
- shortenModel(ctx.model?.id ?? "no-model"),
123
- presentation.truncationLength,
124
- presentation.truncationSymbol,
125
- presentation.truncationDirection,
126
- );
127
- return segment(name, model, config, "accent", "header");
128
- }
129
- case "thinking":
130
- return segment(
131
- name,
132
- runtime.thinkingLevel,
133
- config,
134
- thinkingColor(runtime.thinkingLevel),
135
- "header",
136
- );
137
- case "branch": {
138
- const branch = footerData.getGitBranch();
139
- const pr = branch ? prContextFromStatuses(footerData.getExtensionStatuses()) : undefined;
140
- return segment(
141
- name,
142
- formatGitBranchValue(branch, runtime.gitStatus, pr),
143
- config,
144
- "accent",
145
- "git",
146
- );
147
- }
148
- case "cwd":
149
- return segment(
150
- name,
151
- formatDirectoryPath(ctx.cwd, runtime.homeDir, runtime.gitStatus?.root),
152
- config,
153
- "accent",
154
- "directory",
155
- );
156
- case "tools": {
157
- const activity = formatToolActivity(runtime);
158
- return activity ? segment(name, activity, config, "accent", "runtime") : undefined;
159
- }
160
- case "context": {
161
- const usage = ctx.getContextUsage();
162
- const percentage =
163
- usage?.percent === null || usage?.percent === undefined
164
- ? "?"
165
- : `${usage.percent.toFixed(1)}%`;
166
- const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
167
- return segment(
168
- name,
169
- `${percentage}/${formatCount(contextWindow)}`,
170
- config,
171
- contextColor(usage?.percent),
172
- "runtime",
173
- );
174
- }
175
- case "tokens": {
176
- const value =
177
- usageSummary.input === 0 && usageSummary.output === 0
178
- ? "tok 0"
179
- : `↑${formatCount(usageSummary.input)} ↓${formatCount(usageSummary.output)}`;
180
- return segment(name, value, config, "accent", "runtime");
181
- }
182
- case "cache": {
183
- if (usageSummary.cacheRead === 0 && usageSummary.cacheWrite === 0) return undefined;
184
- const values: string[] = [];
185
- if (usageSummary.cacheRead > 0) values.push(`R${formatCount(usageSummary.cacheRead)}`);
186
- if (usageSummary.cacheWrite > 0) values.push(`W${formatCount(usageSummary.cacheWrite)}`);
187
- if (usageSummary.latestCacheHitRate !== undefined) {
188
- values.push(`CH${usageSummary.latestCacheHitRate.toFixed(1)}%`);
189
- }
190
- return segment(name, values.join(" "), config, "accent", "runtime");
191
- }
192
- case "cost": {
193
- const subscription = isSubscriptionBacked(ctx) ? " (sub)" : "";
194
- return segment(
195
- name,
196
- `${usageSummary.cost.toFixed(usageSummary.cost >= 1 ? 2 : 3)}${subscription}`,
197
- config,
198
- "accent",
199
- "meter",
200
- );
201
- }
202
- case "time":
203
- return segment(name, formatTime(), config, "accent", "meter");
204
- case "turn":
205
- return segment(name, `${runtime.turnCount}`, config, "accent", "meter");
206
- }
108
+ switch (name) {
109
+ case "brand":
110
+ return segment(name, "π", config, "accent", "header", true);
111
+ case "provider":
112
+ return segment(name, ctx.model?.provider ?? "no-provider", config, "accent", "header");
113
+ case "model": {
114
+ const presentation = config.segmentText.model;
115
+ const model = truncateModel(
116
+ shortenModel(ctx.model?.id ?? "no-model"),
117
+ presentation.truncationLength,
118
+ presentation.truncationSymbol,
119
+ presentation.truncationDirection,
120
+ );
121
+ return segment(name, model, config, "accent", "header");
122
+ }
123
+ case "thinking":
124
+ return segment(name, runtime.thinkingLevel, config, thinkingColor(runtime.thinkingLevel), "header");
125
+ case "branch": {
126
+ const branch = footerData.getGitBranch();
127
+ const pr = branch ? prContextFromStatuses(footerData.getExtensionStatuses()) : undefined;
128
+ return segment(name, formatGitBranchValue(branch, runtime.gitStatus, pr), config, "accent", "git");
129
+ }
130
+ case "cwd":
131
+ return segment(
132
+ name,
133
+ formatDirectoryPath(ctx.cwd, runtime.homeDir, runtime.gitStatus?.root),
134
+ config,
135
+ "accent",
136
+ "directory",
137
+ );
138
+ case "tools": {
139
+ const activity = formatToolActivity(runtime);
140
+ return activity ? segment(name, activity, config, "accent", "runtime") : undefined;
141
+ }
142
+ case "context": {
143
+ const usage = ctx.getContextUsage();
144
+ const percentage = usage?.percent === null || usage?.percent === undefined ? "?" : `${usage.percent.toFixed(1)}%`;
145
+ const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
146
+ return segment(
147
+ name,
148
+ `${percentage}/${formatCount(contextWindow)}`,
149
+ config,
150
+ contextColor(usage?.percent),
151
+ "runtime",
152
+ );
153
+ }
154
+ case "tokens": {
155
+ const value =
156
+ usageSummary.input === 0 && usageSummary.output === 0
157
+ ? "tok 0"
158
+ : `↑${formatCount(usageSummary.input)} ↓${formatCount(usageSummary.output)}`;
159
+ return segment(name, value, config, "accent", "runtime");
160
+ }
161
+ case "cache": {
162
+ if (usageSummary.cacheRead === 0 && usageSummary.cacheWrite === 0) return undefined;
163
+ const values: string[] = [];
164
+ if (usageSummary.cacheRead > 0) values.push(`R${formatCount(usageSummary.cacheRead)}`);
165
+ if (usageSummary.cacheWrite > 0) values.push(`W${formatCount(usageSummary.cacheWrite)}`);
166
+ if (usageSummary.latestCacheHitRate !== undefined) {
167
+ values.push(`CH${usageSummary.latestCacheHitRate.toFixed(1)}%`);
168
+ }
169
+ return segment(name, values.join(" "), config, "accent", "runtime");
170
+ }
171
+ case "cost": {
172
+ const subscription = isSubscriptionBacked(ctx) ? " (sub)" : "";
173
+ return segment(
174
+ name,
175
+ `${usageSummary.cost.toFixed(usageSummary.cost >= 1 ? 2 : 3)}${subscription}`,
176
+ config,
177
+ "accent",
178
+ "meter",
179
+ );
180
+ }
181
+ case "time":
182
+ return segment(name, formatTime(), config, "accent", "meter");
183
+ case "turn":
184
+ return segment(name, `${runtime.turnCount}`, config, "accent", "meter");
185
+ }
207
186
  }
208
187
 
209
188
  function segment(
210
- name: SegmentName,
211
- value: string,
212
- config: StatuslineConfig,
213
- color: RenderSegment["color"],
214
- block: PowerlineBlockName,
215
- emphasis = false,
189
+ name: SegmentName,
190
+ value: string,
191
+ config: StatuslineConfig,
192
+ color: RenderSegment["color"],
193
+ block: PowerlineBlockName,
194
+ emphasis = false,
216
195
  ): RenderSegment {
217
- return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
196
+ return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
218
197
  }
219
198
 
220
199
  export function formatConfiguredSegment(
221
- name: SegmentName,
222
- value: string,
223
- config: Pick<StatuslineConfig, "segmentText">,
200
+ name: SegmentName,
201
+ value: string,
202
+ config: Pick<StatuslineConfig, "segmentText">,
224
203
  ): string {
225
- const presentation = config.segmentText[name];
226
- return `${presentation.prefix}${value}${presentation.suffix}`;
204
+ const presentation = config.segmentText[name];
205
+ return `${presentation.prefix}${value}${presentation.suffix}`;
227
206
  }
228
207
 
229
208
  function thinkingColor(level: ThinkingLevel): ThemeColor {
230
- switch (level as string) {
231
- case "off":
232
- return "dim";
233
- case "minimal":
234
- return "thinkingMinimal";
235
- case "low":
236
- return "thinkingLow";
237
- case "medium":
238
- return "thinkingMedium";
239
- case "high":
240
- return "thinkingHigh";
241
- case "xhigh":
242
- return "thinkingXhigh";
243
- case "max":
244
- return "thinkingMax" as ThemeColor;
245
- default:
246
- return "dim";
247
- }
209
+ switch (level as string) {
210
+ case "off":
211
+ return "dim";
212
+ case "minimal":
213
+ return "thinkingMinimal";
214
+ case "low":
215
+ return "thinkingLow";
216
+ case "medium":
217
+ return "thinkingMedium";
218
+ case "high":
219
+ return "thinkingHigh";
220
+ case "xhigh":
221
+ return "thinkingXhigh";
222
+ case "max":
223
+ return "thinkingMax" as ThemeColor;
224
+ default:
225
+ return "dim";
226
+ }
248
227
  }
249
228
 
250
229
  export function contextColor(percent: number | null | undefined): ThemeColor {
251
- if (percent === null || percent === undefined) return "dim";
252
- if (percent >= 90) return "error";
253
- if (percent >= 70) return "warning";
254
- return "success";
230
+ if (percent === null || percent === undefined) return "dim";
231
+ if (percent >= 90) return "error";
232
+ if (percent >= 70) return "warning";
233
+ return "success";
255
234
  }
256
235
 
257
236
  const MAX_UI_PROMPT_TITLE_CODE_POINTS = 256;
258
237
  const MAX_UI_PROMPT_TITLE_WIDTH = 40;
259
238
 
260
239
  function boundUIPromptTitleLength(title: string): string {
261
- let end = 0;
262
- let ellipsisEnd = 0;
263
- let codePoints = 0;
264
- while (end < title.length && codePoints < MAX_UI_PROMPT_TITLE_CODE_POINTS) {
265
- const codePoint = title.codePointAt(end) ?? 0;
266
- end += codePoint > 0xffff ? 2 : 1;
267
- codePoints += 1;
268
- if (codePoints < MAX_UI_PROMPT_TITLE_CODE_POINTS) ellipsisEnd = end;
269
- }
270
- return end < title.length ? `${title.slice(0, ellipsisEnd)}…` : title;
240
+ let end = 0;
241
+ let ellipsisEnd = 0;
242
+ let codePoints = 0;
243
+ while (end < title.length && codePoints < MAX_UI_PROMPT_TITLE_CODE_POINTS) {
244
+ const codePoint = title.codePointAt(end) ?? 0;
245
+ end += codePoint > 0xffff ? 2 : 1;
246
+ codePoints += 1;
247
+ if (codePoints < MAX_UI_PROMPT_TITLE_CODE_POINTS) ellipsisEnd = end;
248
+ }
249
+ return end < title.length ? `${title.slice(0, ellipsisEnd)}…` : title;
271
250
  }
272
251
 
273
252
  function formatUIPromptTitle(title: string | undefined): string {
274
- const safeTitle = title ? sanitizeTerminalText(title).trim() : "";
275
- const boundedTitle = boundUIPromptTitleLength(safeTitle);
276
- if (visibleWidth(boundedTitle) <= MAX_UI_PROMPT_TITLE_WIDTH) return boundedTitle;
277
- return `${sliceByColumn(boundedTitle, 0, MAX_UI_PROMPT_TITLE_WIDTH - 1, true)}…`;
253
+ const safeTitle = title ? sanitizeTerminalText(title).trim() : "";
254
+ const boundedTitle = boundUIPromptTitleLength(safeTitle);
255
+ if (visibleWidth(boundedTitle) <= MAX_UI_PROMPT_TITLE_WIDTH) return boundedTitle;
256
+ return `${sliceByColumn(boundedTitle, 0, MAX_UI_PROMPT_TITLE_WIDTH - 1, true)}…`;
278
257
  }
279
258
 
280
259
  export function formatToolActivity(runtime: RuntimeState): string | undefined {
281
- if (runtime.uiPrompt) {
282
- const title = formatUIPromptTitle(runtime.uiPrompt.title);
283
- return `⌨ waiting for ${runtime.uiPrompt.kind}${title ? ` · ${title}` : ""}`;
284
- }
260
+ if (runtime.uiPrompt) {
261
+ const title = formatUIPromptTitle(runtime.uiPrompt.title);
262
+ return `⌨ waiting for ${runtime.uiPrompt.kind}${title ? ` · ${title}` : ""}`;
263
+ }
285
264
 
286
- const active = [...runtime.activeTools.entries()];
287
- if (active.length > 0) {
288
- const [name, count] = active[0] ?? ["tool", 1];
289
- const suffix = count > 1 ? `×${count}` : active.length > 1 ? `+${active.length - 1}` : "";
290
- return `⚙️ ${name}${suffix}`;
291
- }
265
+ const active = [...runtime.activeTools.entries()];
266
+ if (active.length > 0) {
267
+ const [name, count] = active[0] ?? ["tool", 1];
268
+ const suffix = count > 1 ? `×${count}` : active.length > 1 ? `+${active.length - 1}` : "";
269
+ return `⚙️ ${name}${suffix}`;
270
+ }
292
271
 
293
- return runtime.isStreaming ? "💭 thinking" : undefined;
272
+ return runtime.isStreaming ? "💭 thinking" : undefined;
294
273
  }
295
274
 
296
275
  export function prLinkFromStatuses(statuses: ReadonlyMap<string, string>): string | undefined {
297
- const value = statuses.get(GITHUB_PR_KEY);
298
- if (!value) return undefined;
299
- // Extract the OSC 8 hyperlink span (the clickable "#123"); skip non-PR states
300
- // like "PR gh missing" that carry no link. github-pr emits exactly one link, so the
301
- // first OSC 8 span is the PR number.
302
- const open = value.indexOf("\x1b]8;;");
303
- if (open === -1) return undefined;
304
- const closeMarker = "\x1b]8;;\x07";
305
- const close = value.indexOf(closeMarker, open + 1);
306
- return close === -1 ? undefined : value.slice(open, close + closeMarker.length);
276
+ const value = statuses.get(GITHUB_PR_KEY);
277
+ if (!value) return undefined;
278
+ // Extract the OSC 8 hyperlink span (the clickable "#123"); skip non-PR states
279
+ // like "PR gh missing" that carry no link. github-pr emits exactly one link, so the
280
+ // first OSC 8 span is the PR number.
281
+ const open = value.indexOf("\x1b]8;;");
282
+ if (open === -1) return undefined;
283
+ const closeMarker = "\x1b]8;;\x07";
284
+ const close = value.indexOf(closeMarker, open + 1);
285
+ return close === -1 ? undefined : value.slice(open, close + closeMarker.length);
307
286
  }
308
287
 
309
288
  export function prContextFromStatuses(statuses: ReadonlyMap<string, string>): string | undefined {
310
- const value = statuses.get(GITHUB_PR_KEY);
311
- if (!value) return undefined;
312
- const link = prLinkFromStatuses(statuses);
313
- const reference = link ?? plainPrReference(value);
314
- if (!reference) return undefined;
289
+ const value = statuses.get(GITHUB_PR_KEY);
290
+ if (!value) return undefined;
291
+ const link = prLinkFromStatuses(statuses);
292
+ const reference = link ?? plainPrReference(value);
293
+ if (!reference) return undefined;
315
294
 
316
- const state = compactPrState(link ? value.replace(link, "") : value);
317
- return state ? `${reference} · ${state}` : undefined;
295
+ const state = compactPrState(link ? value.replace(link, "") : value);
296
+ return state ? `${reference} · ${state}` : undefined;
318
297
  }
319
298
 
320
299
  function plainPrReference(value: string): string | undefined {
321
- return /^PR\s+(#\d+):/u.exec(value)?.[1];
300
+ return /^PR\s+(#\d+):/u.exec(value)?.[1];
322
301
  }
323
302
 
324
303
  function compactPrState(value: string): string | undefined {
325
- if (/:\s*merged\s*$/.test(value)) return "merged";
326
- if (/:\s*closed\s*$/.test(value)) return "closed";
327
- if (/\bdraft\b/.test(value)) return "draft";
304
+ if (/:\s*merged\s*$/.test(value)) return "merged";
305
+ if (/:\s*closed\s*$/.test(value)) return "closed";
306
+ if (/\bdraft\b/.test(value)) return "draft";
328
307
 
329
- const failing = /\bchecks failing \((\d+)\)/.exec(value);
330
- if (failing) return `${failing[1]} failing`;
331
- if (/\bchanges requested\b/.test(value)) return "changes requested";
308
+ const failing = /\bchecks failing \((\d+)\)/.exec(value);
309
+ if (failing) return `${failing[1]} failing`;
310
+ if (/\bchanges requested\b/.test(value)) return "changes requested";
332
311
 
333
- const pending = /\bchecks pending \((\d+)\)/.exec(value);
334
- if (pending) return `${pending[1]} pending`;
335
- if (/\bapproved\b/.test(value)) return "approved";
336
- if (/\breview required\b/.test(value)) return "review required";
337
- if (/\bchecks passing\b/.test(value)) return "checks passing";
338
- if (/\bno checks\b/.test(value)) return "no checks";
339
- return undefined;
312
+ const pending = /\bchecks pending \((\d+)\)/.exec(value);
313
+ if (pending) return `${pending[1]} pending`;
314
+ if (/\bapproved\b/.test(value)) return "approved";
315
+ if (/\breview required\b/.test(value)) return "review required";
316
+ if (/\bchecks passing\b/.test(value)) return "checks passing";
317
+ if (/\bno checks\b/.test(value)) return "no checks";
318
+ return undefined;
340
319
  }
341
320
 
342
321
  function isSubscriptionBacked(ctx: ExtensionContext): boolean {
343
- const model = ctx.model;
344
- return (
345
- model !== undefined &&
346
- (model.provider === "kimi-coding" || ctx.modelRegistry.isUsingOAuth(model))
347
- );
322
+ const model = ctx.model;
323
+ return model !== undefined && (model.provider === "kimi-coding" || ctx.modelRegistry.isUsingOAuth(model));
348
324
  }
349
325
 
350
326
  export function formatCount(value: number): string {
351
- if (value < 1000) return `${value}`;
352
- if (value < 1_000_000) return `${(value / 1000).toFixed(value < 10_000 ? 1 : 0)}k`;
353
- return `${(value / 1_000_000).toFixed(1)}m`;
327
+ if (value < 1000) return `${value}`;
328
+ if (value < 1_000_000) return `${(value / 1000).toFixed(value < 10_000 ? 1 : 0)}k`;
329
+ return `${(value / 1_000_000).toFixed(1)}m`;
354
330
  }
355
331
 
356
332
  function formatTime(): string {
357
- const now = new Date();
358
- const hours = now.getHours().toString().padStart(2, "0");
359
- const minutes = now.getMinutes().toString().padStart(2, "0");
360
- return `${hours}:${minutes}`;
333
+ const now = new Date();
334
+ const hours = now.getHours().toString().padStart(2, "0");
335
+ const minutes = now.getMinutes().toString().padStart(2, "0");
336
+ return `${hours}:${minutes}`;
361
337
  }
362
338
 
363
339
  const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
364
340
 
365
- export function truncateModel(
366
- model: string,
367
- length: number,
368
- symbol: string,
369
- direction: TruncationDirection,
370
- ): string {
371
- const safeModel = sanitizeTerminalText(model);
372
- if (length === 0) return safeModel;
373
- const graphemes = [...graphemeSegmenter.segment(safeModel)].map(({ segment }) => segment);
374
- if (graphemes.length <= length) return safeModel;
375
- const safeSymbol = sanitizeTerminalText(symbol);
341
+ export function truncateModel(model: string, length: number, symbol: string, direction: TruncationDirection): string {
342
+ const safeModel = sanitizeTerminalText(model);
343
+ if (length === 0) return safeModel;
344
+ const graphemes = [...graphemeSegmenter.segment(safeModel)].map(({ segment }) => segment);
345
+ if (graphemes.length <= length) return safeModel;
346
+ const safeSymbol = sanitizeTerminalText(symbol);
376
347
 
377
- switch (direction) {
378
- case "start":
379
- return `${safeSymbol}${graphemes.slice(-length).join("")}`;
380
- case "middle": {
381
- const headLength = Math.ceil(length / 2);
382
- const tailLength = Math.floor(length / 2);
383
- const tail = tailLength > 0 ? graphemes.slice(-tailLength).join("") : "";
384
- return `${graphemes.slice(0, headLength).join("")}${safeSymbol}${tail}`;
385
- }
386
- case "end":
387
- return `${graphemes.slice(0, length).join("")}${safeSymbol}`;
388
- }
348
+ switch (direction) {
349
+ case "start":
350
+ return `${safeSymbol}${graphemes.slice(-length).join("")}`;
351
+ case "middle": {
352
+ const headLength = Math.ceil(length / 2);
353
+ const tailLength = Math.floor(length / 2);
354
+ const tail = tailLength > 0 ? graphemes.slice(-tailLength).join("") : "";
355
+ return `${graphemes.slice(0, headLength).join("")}${safeSymbol}${tail}`;
356
+ }
357
+ case "end":
358
+ return `${graphemes.slice(0, length).join("")}${safeSymbol}`;
359
+ }
389
360
  }
390
361
 
391
362
  export function shortenModel(model: string): string {
392
- return model
393
- .replace(/^claude-/, "")
394
- .replace(/^gpt-/, "gpt ")
395
- .replace(/-20\d{6}$/, "")
396
- .replace(/-latest$/, "");
363
+ return model
364
+ .replace(/^claude-/, "")
365
+ .replace(/^gpt-/, "gpt ")
366
+ .replace(/-20\d{6}$/, "")
367
+ .replace(/-latest$/, "");
397
368
  }