@narumitw/pi-statusline 0.49.15 → 0.50.1

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