@narumitw/pi-statusline 0.50.0 → 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 +29 -209
- 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 +21 -76
- package/dist/index.ts.map +2 -2
- package/docs/configuration.md +194 -0
- package/package.json +51 -51
- package/src/ansi.ts +18 -24
- 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 -219
- package/src/git-status.ts +103 -111
- package/src/information-profiles.ts +12 -31
- package/src/powerline.ts +120 -127
- 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 +277 -306
- package/src/settings.ts +516 -568
- package/src/statusline.ts +347 -372
- 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,397 +1,368 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
48
|
+
if (width <= 0) return "";
|
|
53
49
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
74
|
+
return renderPowerlineStatusline(width, segments, config, trueColor);
|
|
81
75
|
}
|
|
82
76
|
|
|
83
77
|
export function renderExtensionStatusline(
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
101
|
+
name: SegmentName,
|
|
102
|
+
ctx: ExtensionContext,
|
|
103
|
+
footerData: ReadonlyFooterDataProvider,
|
|
104
|
+
config: StatuslineConfig,
|
|
105
|
+
runtime: RuntimeState,
|
|
106
|
+
usageSummary: FooterUsageSummary,
|
|
113
107
|
): RenderSegment | undefined {
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
189
|
+
name: SegmentName,
|
|
190
|
+
value: string,
|
|
191
|
+
config: StatuslineConfig,
|
|
192
|
+
color: RenderSegment["color"],
|
|
193
|
+
block: PowerlineBlockName,
|
|
194
|
+
emphasis = false,
|
|
216
195
|
): RenderSegment {
|
|
217
|
-
|
|
196
|
+
return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
|
|
218
197
|
}
|
|
219
198
|
|
|
220
199
|
export function formatConfiguredSegment(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
200
|
+
name: SegmentName,
|
|
201
|
+
value: string,
|
|
202
|
+
config: Pick<StatuslineConfig, "segmentText">,
|
|
224
203
|
): string {
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
272
|
+
return runtime.isStreaming ? "💭 thinking" : undefined;
|
|
294
273
|
}
|
|
295
274
|
|
|
296
275
|
export function prLinkFromStatuses(statuses: ReadonlyMap<string, string>): string | undefined {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
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
|
-
|
|
300
|
+
return /^PR\s+(#\d+):/u.exec(value)?.[1];
|
|
322
301
|
}
|
|
323
302
|
|
|
324
303
|
function compactPrState(value: string): string | undefined {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
344
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
363
|
+
return model
|
|
364
|
+
.replace(/^claude-/, "")
|
|
365
|
+
.replace(/^gpt-/, "gpt ")
|
|
366
|
+
.replace(/-20\d{6}$/, "")
|
|
367
|
+
.replace(/-latest$/, "");
|
|
397
368
|
}
|