@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/README.md +47 -222
- package/dist/chunks/{chunk-74QZGJ7L.js → chunk-J2ZSGEDQ.js} +18 -97
- package/dist/chunks/chunk-J2ZSGEDQ.js.map +7 -0
- package/dist/chunks/{commands-C2G2G7C6.js → commands-PPT3AJTL.js} +14 -58
- package/dist/chunks/commands-PPT3AJTL.js.map +7 -0
- package/dist/index.ts +101 -89
- package/dist/index.ts.map +3 -3
- package/docs/configuration.md +194 -0
- package/package.json +51 -54
- package/src/ansi.ts +23 -18
- package/src/command-contract.ts +6 -6
- package/src/commands.ts +759 -818
- package/src/directory.ts +27 -31
- package/src/extension-status.ts +186 -214
- package/src/git-status.ts +103 -111
- package/src/information-profiles.ts +12 -31
- package/src/powerline.ts +120 -121
- package/src/presets/candy.ts +1 -7
- package/src/presets/create-ramp.ts +21 -23
- package/src/presets/custom.ts +7 -7
- package/src/presets/forest.ts +1 -7
- package/src/presets/index.ts +30 -32
- package/src/presets/mono.ts +1 -7
- package/src/presets/neon.ts +1 -7
- package/src/presets/ocean.ts +1 -7
- package/src/presets/sunset.ts +1 -7
- package/src/presets/tokyo-night.ts +9 -9
- package/src/presets/types.ts +5 -5
- package/src/render.ts +291 -280
- package/src/settings.ts +516 -568
- package/src/statusline.ts +348 -341
- package/src/types.ts +34 -42
- package/src/usage.ts +40 -40
- package/dist/chunks/chunk-74QZGJ7L.js.map +0 -7
- package/dist/chunks/commands-C2G2G7C6.js.map +0 -7
package/src/render.ts
CHANGED
|
@@ -1,357 +1,368 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
48
|
+
if (width <= 0) return "";
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
74
|
+
return renderPowerlineStatusline(width, segments, config, trueColor);
|
|
77
75
|
}
|
|
78
76
|
|
|
79
77
|
export function renderExtensionStatusline(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
101
|
+
name: SegmentName,
|
|
102
|
+
ctx: ExtensionContext,
|
|
103
|
+
footerData: ReadonlyFooterDataProvider,
|
|
104
|
+
config: StatuslineConfig,
|
|
105
|
+
runtime: RuntimeState,
|
|
106
|
+
usageSummary: FooterUsageSummary,
|
|
107
107
|
): RenderSegment | undefined {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
189
|
+
name: SegmentName,
|
|
190
|
+
value: string,
|
|
191
|
+
config: StatuslineConfig,
|
|
192
|
+
color: RenderSegment["color"],
|
|
193
|
+
block: PowerlineBlockName,
|
|
194
|
+
emphasis = false,
|
|
210
195
|
): RenderSegment {
|
|
211
|
-
|
|
196
|
+
return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
|
|
212
197
|
}
|
|
213
198
|
|
|
214
199
|
export function formatConfiguredSegment(
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
200
|
+
name: SegmentName,
|
|
201
|
+
value: string,
|
|
202
|
+
config: Pick<StatuslineConfig, "segmentText">,
|
|
218
203
|
): string {
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
272
|
+
return runtime.isStreaming ? "💭 thinking" : undefined;
|
|
260
273
|
}
|
|
261
274
|
|
|
262
275
|
export function prLinkFromStatuses(statuses: ReadonlyMap<string, string>): string | undefined {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
304
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
363
|
+
return model
|
|
364
|
+
.replace(/^claude-/, "")
|
|
365
|
+
.replace(/^gpt-/, "gpt ")
|
|
366
|
+
.replace(/-20\d{6}$/, "")
|
|
367
|
+
.replace(/-latest$/, "");
|
|
357
368
|
}
|