@narumitw/pi-statusline 0.49.9 → 0.49.11
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 +16 -3
- package/dist/chunks/chunk-4PIP7RGK.ts +768 -0
- package/dist/chunks/chunk-4PIP7RGK.ts.map +7 -0
- package/dist/chunks/commands-5CMKUNTM.ts +752 -0
- package/dist/chunks/commands-5CMKUNTM.ts.map +7 -0
- package/dist/index.ts +1069 -0
- package/dist/index.ts.map +7 -0
- package/package.json +8 -4
- package/src/directory.ts +1 -1
- package/src/render.ts +1 -1
package/dist/index.ts
ADDED
|
@@ -0,0 +1,1069 @@
|
|
|
1
|
+
// @generated by scripts/build-runtime.mjs; do not edit.
|
|
2
|
+
// @ts-nocheck -- generated JavaScript uses a .ts extension for Pi's Jiti loader.
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_EXTENSION_STATUS_ICONS,
|
|
5
|
+
LINE_BREAK_SEGMENT_NAME,
|
|
6
|
+
completeStatuslineArguments,
|
|
7
|
+
consumeStatuslineSettingsNotice,
|
|
8
|
+
loadStatuslineSettings,
|
|
9
|
+
loadStatuslineSettingsForAgent,
|
|
10
|
+
resolvePreset,
|
|
11
|
+
settingsFilePath
|
|
12
|
+
} from "./chunks/chunk-4PIP7RGK.js";
|
|
13
|
+
|
|
14
|
+
// src/statusline.ts
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import {
|
|
17
|
+
getAgentDir as getAgentDir2
|
|
18
|
+
} from "@earendil-works/pi-coding-agent";
|
|
19
|
+
|
|
20
|
+
// src/extension-status.ts
|
|
21
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
22
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
23
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
25
|
+
|
|
26
|
+
// src/powerline.ts
|
|
27
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
28
|
+
|
|
29
|
+
// src/ansi.ts
|
|
30
|
+
function ansiStyle(text, colors) {
|
|
31
|
+
const codes = [
|
|
32
|
+
colors.fg ? truecolorCode("38", colors.fg) : void 0,
|
|
33
|
+
colors.bg ? truecolorCode("48", colors.bg) : void 0
|
|
34
|
+
].filter((code) => code !== void 0);
|
|
35
|
+
if (codes.length === 0) return text;
|
|
36
|
+
return `\x1B[${codes.join(";")}m${text}\x1B[0m`;
|
|
37
|
+
}
|
|
38
|
+
function truecolorCode(prefix, hex) {
|
|
39
|
+
const { red, green, blue } = hexToRgb(hex);
|
|
40
|
+
return `${prefix};2;${red};${green};${blue}`;
|
|
41
|
+
}
|
|
42
|
+
function hexToRgb(hex) {
|
|
43
|
+
const normalized = hex.replace(/^#/, "");
|
|
44
|
+
return {
|
|
45
|
+
red: Number.parseInt(normalized.slice(0, 2), 16),
|
|
46
|
+
green: Number.parseInt(normalized.slice(2, 4), 16),
|
|
47
|
+
blue: Number.parseInt(normalized.slice(4, 6), 16)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/powerline.ts
|
|
52
|
+
function renderPowerlineStatusline(width, items, config) {
|
|
53
|
+
if (items.length === 0 || width <= 0) return "";
|
|
54
|
+
return splitLines(items).map((segments) => fitPowerlineSegments(segments, width, config)).join("\n");
|
|
55
|
+
}
|
|
56
|
+
function splitLines(items) {
|
|
57
|
+
const lines = [[]];
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
if (item.name === LINE_BREAK_SEGMENT_NAME) lines.push([]);
|
|
60
|
+
else lines.at(-1)?.push(item);
|
|
61
|
+
}
|
|
62
|
+
return lines;
|
|
63
|
+
}
|
|
64
|
+
var SEGMENT_RETENTION_PRIORITY = {
|
|
65
|
+
context: 120,
|
|
66
|
+
model: 110,
|
|
67
|
+
branch: 100,
|
|
68
|
+
tools: 90,
|
|
69
|
+
cwd: 80,
|
|
70
|
+
thinking: 70,
|
|
71
|
+
cost: 60,
|
|
72
|
+
provider: 50,
|
|
73
|
+
cache: 45,
|
|
74
|
+
tokens: 40,
|
|
75
|
+
time: 30,
|
|
76
|
+
turn: 20,
|
|
77
|
+
brand: 10
|
|
78
|
+
};
|
|
79
|
+
function fitPowerlineSegments(segments, width, config) {
|
|
80
|
+
if (segments.length === 0) return "";
|
|
81
|
+
const fitted = [...segments];
|
|
82
|
+
while (fitted.length > 1) {
|
|
83
|
+
const rendered2 = joinPowerlineSegments(fitted, config);
|
|
84
|
+
if (visibleWidth(rendered2) <= width) return rendered2;
|
|
85
|
+
let removalIndex = 0;
|
|
86
|
+
for (let index = 1; index < fitted.length; index += 1) {
|
|
87
|
+
const candidate = fitted[index];
|
|
88
|
+
const current = fitted[removalIndex];
|
|
89
|
+
if (candidate && current && SEGMENT_RETENTION_PRIORITY[candidate.name] < SEGMENT_RETENTION_PRIORITY[current.name]) {
|
|
90
|
+
removalIndex = index;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
fitted.splice(removalIndex, 1);
|
|
94
|
+
}
|
|
95
|
+
const rendered = joinPowerlineSegments(fitted, config);
|
|
96
|
+
return visibleWidth(rendered) <= width ? rendered : "";
|
|
97
|
+
}
|
|
98
|
+
function powerlineExtensionSeparator(_theme, palettePreset = "tokyo-night") {
|
|
99
|
+
return ansiStyle(" \u2022 ", { fg: resolvePreset(palettePreset).extensionSeparator });
|
|
100
|
+
}
|
|
101
|
+
function joinPowerlineSegments(segments, config) {
|
|
102
|
+
const preset = resolvePreset(config.palettePreset);
|
|
103
|
+
const blocks = contiguousBlocks(segments, preset, config.palettePreset, config.palette);
|
|
104
|
+
let line = ansiStyle("\u2591\u2592\u2593", { fg: preset.lead });
|
|
105
|
+
for (const [index, block] of blocks.entries()) {
|
|
106
|
+
const previous = index === 0 ? void 0 : blocks[index - 1]?.colors;
|
|
107
|
+
if (previous) line += ansiStyle("\uE0B4", { fg: previous.bg, bg: block.colors.bg });
|
|
108
|
+
line += ansiStyle(formatBlockText(block, config), block.colors);
|
|
109
|
+
}
|
|
110
|
+
const lastBlock = blocks.at(-1);
|
|
111
|
+
if (lastBlock) line += ansiStyle("\uE0B4", { fg: lastBlock.colors.bg });
|
|
112
|
+
return line;
|
|
113
|
+
}
|
|
114
|
+
function contiguousBlocks(segments, preset, palettePreset, configuredPalette) {
|
|
115
|
+
const blocks = [];
|
|
116
|
+
const usesConfiguredColors = palettePreset === "custom";
|
|
117
|
+
for (const segment2 of segments) {
|
|
118
|
+
const colors = usesConfiguredColors ? configuredPalette[segment2.name] ?? {} : preset.blocks[segment2.block];
|
|
119
|
+
const previous = blocks.at(-1);
|
|
120
|
+
const matchesPrevious = previous !== void 0 && (usesConfiguredColors ? colorsEqual(previous.colors, colors) : previous.baseBlock === segment2.block);
|
|
121
|
+
if (matchesPrevious) previous.segments.push(segment2);
|
|
122
|
+
else blocks.push({ baseBlock: segment2.block, colors, segments: [segment2] });
|
|
123
|
+
}
|
|
124
|
+
return blocks;
|
|
125
|
+
}
|
|
126
|
+
function colorsEqual(left, right) {
|
|
127
|
+
return left.fg === right.fg && left.bg === right.bg;
|
|
128
|
+
}
|
|
129
|
+
function formatBlockText(block, config) {
|
|
130
|
+
const texts = block.segments.map(formatSegmentText);
|
|
131
|
+
const separator = separatorText(config.separator, config.density === "cozy");
|
|
132
|
+
const leading = config.density === "cozy" ? " " : " ";
|
|
133
|
+
const trailing = config.density === "cozy" ? " " : "";
|
|
134
|
+
return `${leading}${texts.join(separator)}${trailing}`;
|
|
135
|
+
}
|
|
136
|
+
function formatSegmentText(segment2) {
|
|
137
|
+
return segment2.emphasis ? `\x1B[1m${segment2.text}\x1B[22m` : segment2.text;
|
|
138
|
+
}
|
|
139
|
+
function separatorText(separator, cozy) {
|
|
140
|
+
const padding = cozy ? " " : " ";
|
|
141
|
+
switch (separator) {
|
|
142
|
+
case "dot":
|
|
143
|
+
return `${padding}\u2022${padding}`;
|
|
144
|
+
case "bar":
|
|
145
|
+
return `${padding}\u2502${padding}`;
|
|
146
|
+
case "powerline":
|
|
147
|
+
return `${padding}\uE0B1${padding}`;
|
|
148
|
+
case "round":
|
|
149
|
+
return `${padding}\u276F${padding}`;
|
|
150
|
+
case "none":
|
|
151
|
+
return padding;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/extension-status.ts
|
|
156
|
+
var STATUSLINE_KEY = "statusline";
|
|
157
|
+
var COMPATIBLE_STATUS_ICON_KEYS = {
|
|
158
|
+
retry: "unknown-error-retry",
|
|
159
|
+
sync: "pisync",
|
|
160
|
+
"unknown-error-retry": "retry",
|
|
161
|
+
pisync: "sync"
|
|
162
|
+
};
|
|
163
|
+
var EMPTY_EXTENSION_STATUS_ICON_ALIASES = /* @__PURE__ */ new Map();
|
|
164
|
+
function extensionStatusSeparator(config, theme) {
|
|
165
|
+
return powerlineExtensionSeparator(theme, config.palettePreset);
|
|
166
|
+
}
|
|
167
|
+
function formatExtensionStatuses(statuses, theme, config, runtime, hiddenKeys = /* @__PURE__ */ new Set()) {
|
|
168
|
+
const separator = extensionStatusSeparator(config, theme);
|
|
169
|
+
const visibleStatuses = [
|
|
170
|
+
...formatDuplicateExtensionStatus(runtime, theme),
|
|
171
|
+
...[...statuses.entries()].filter(
|
|
172
|
+
([key, value]) => key !== STATUSLINE_KEY && !hiddenKeys.has(key) && value.trim().length > 0
|
|
173
|
+
).map(
|
|
174
|
+
([key, value]) => formatExtensionStatus(key, value, theme, config, runtime.extensionStatusIconAliases)
|
|
175
|
+
)
|
|
176
|
+
].slice(0, 5);
|
|
177
|
+
return visibleStatuses.join(separator);
|
|
178
|
+
}
|
|
179
|
+
function formatExtensionStatus(key, value, theme, config, extensionStatusIconAliases = EMPTY_EXTENSION_STATUS_ICON_ALIASES) {
|
|
180
|
+
const status = splitExtensionStatusIcon(stripExtensionStatusPrefix(key, value));
|
|
181
|
+
const text = simplifyExtensionStatusText(status.text);
|
|
182
|
+
const color = extensionColor(key, value);
|
|
183
|
+
const textColor = color === "warning" ? "warning" : "muted";
|
|
184
|
+
const icon = extensionStatusIcon(
|
|
185
|
+
key,
|
|
186
|
+
status.icon,
|
|
187
|
+
config.extensionStatusIcons,
|
|
188
|
+
extensionStatusIconAliases
|
|
189
|
+
);
|
|
190
|
+
const renderedText = theme.fg(textColor, text);
|
|
191
|
+
return icon ? `${theme.fg(color, icon)} ${renderedText}` : renderedText;
|
|
192
|
+
}
|
|
193
|
+
function extensionStatusIcon(key, leadingIcon, configuredIcons, extensionStatusIconAliases) {
|
|
194
|
+
if (Object.hasOwn(configuredIcons, key)) return configuredIcons[key];
|
|
195
|
+
const namespaceIcon = configuredNamespaceIcon(key, configuredIcons);
|
|
196
|
+
if (namespaceIcon !== void 0) return namespaceIcon;
|
|
197
|
+
const compatibleKey = COMPATIBLE_STATUS_ICON_KEYS[key];
|
|
198
|
+
if (compatibleKey && Object.hasOwn(configuredIcons, compatibleKey)) {
|
|
199
|
+
return configuredIcons[compatibleKey];
|
|
200
|
+
}
|
|
201
|
+
for (const alias of extensionStatusAliasesForKey(key, extensionStatusIconAliases)) {
|
|
202
|
+
if (Object.hasOwn(configuredIcons, alias)) return configuredIcons[alias];
|
|
203
|
+
}
|
|
204
|
+
return leadingIcon ?? DEFAULT_EXTENSION_STATUS_ICONS[key] ?? "\u{1F50C}";
|
|
205
|
+
}
|
|
206
|
+
function configuredNamespaceIcon(key, configuredIcons) {
|
|
207
|
+
let match;
|
|
208
|
+
for (const [selector, icon] of Object.entries(configuredIcons)) {
|
|
209
|
+
if (!selector.endsWith(":*")) continue;
|
|
210
|
+
const base = selector.slice(0, -2);
|
|
211
|
+
if (!base || !key.startsWith(`${base}:`)) continue;
|
|
212
|
+
if (!match || base.length > match.baseLength) match = { baseLength: base.length, icon };
|
|
213
|
+
}
|
|
214
|
+
return match?.icon;
|
|
215
|
+
}
|
|
216
|
+
function extensionStatusAliasesForKey(key, extensionStatusIconAliases) {
|
|
217
|
+
for (const [statusBase, aliases] of extensionStatusIconAliases) {
|
|
218
|
+
if (statusKeyMatchesStatusBase(key, statusBase)) return aliases;
|
|
219
|
+
}
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
function statusKeyMatchesStatusBase(key, statusBase) {
|
|
223
|
+
return key === statusBase || key.startsWith(`${statusBase}:`) || key.startsWith(`${statusBase}/`);
|
|
224
|
+
}
|
|
225
|
+
function wrapExtensionStatusline(status, width) {
|
|
226
|
+
if (!status || width <= 0) return [];
|
|
227
|
+
return wrapTextWithAnsi(status, width);
|
|
228
|
+
}
|
|
229
|
+
function formatDuplicateExtensionStatus(runtime, theme) {
|
|
230
|
+
if (runtime.duplicateExtensions.length === 0) return [];
|
|
231
|
+
const names = runtime.duplicateExtensions.slice(0, 2).join(", ");
|
|
232
|
+
const suffix = runtime.duplicateExtensions.length > 2 ? ` +${runtime.duplicateExtensions.length - 2}` : "";
|
|
233
|
+
return [`${theme.fg("warning", "\u26A0\uFE0F")} ${theme.fg("warning", `dup ${names}${suffix}`)}`];
|
|
234
|
+
}
|
|
235
|
+
function splitExtensionStatusIcon(value) {
|
|
236
|
+
const trimmed = value.trim();
|
|
237
|
+
const [firstToken, ...restTokens] = trimmed.split(/\s+/);
|
|
238
|
+
if (firstToken && isEmojiOnlyToken(firstToken)) {
|
|
239
|
+
return { icon: firstToken, text: restTokens.join(" ") };
|
|
240
|
+
}
|
|
241
|
+
return { text: trimmed };
|
|
242
|
+
}
|
|
243
|
+
function isEmojiOnlyToken(value) {
|
|
244
|
+
return /^(?=.*(?:\p{Extended_Pictographic}|\p{Regional_Indicator}|[0-9#*]\ufe0f?\u20e3))(?:\p{Extended_Pictographic}|\p{Emoji_Modifier}|\p{Regional_Indicator}|\u200d|\ufe0f|[0-9#*]\ufe0f?\u20e3)+$/u.test(
|
|
245
|
+
value
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
function extensionColor(key, value) {
|
|
249
|
+
const normalized = `${key} ${value}`.toLowerCase();
|
|
250
|
+
if (/missing|error|fail|conflict|duplicate|unavailable/.test(normalized)) return "warning";
|
|
251
|
+
if (normalized.includes("codex")) return "accent";
|
|
252
|
+
if (/ready|active|running|enabled|awake|ok/.test(normalized)) return "success";
|
|
253
|
+
return "muted";
|
|
254
|
+
}
|
|
255
|
+
function stripExtensionStatusPrefix(key, value) {
|
|
256
|
+
return value.trim().replace(new RegExp(`^${escapeRegExp(key)}\\s*:\\s*`, "iu"), "");
|
|
257
|
+
}
|
|
258
|
+
function simplifyExtensionStatusText(value) {
|
|
259
|
+
return value.trim().replace(/\bready\b/giu, "\u2713").replace(/\bmissing\b/giu, "\u2717").replace(/,\s*/g, " ").replace(/\s+\([^)]*\)\s*$/, "").replace(/\s+/g, " ");
|
|
260
|
+
}
|
|
261
|
+
function escapeRegExp(value) {
|
|
262
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
263
|
+
}
|
|
264
|
+
function readInstalledExtensionPackages(cwd) {
|
|
265
|
+
const packages = [];
|
|
266
|
+
const settingsFiles = extensionSettingsFiles(cwd);
|
|
267
|
+
for (const settingsFile of settingsFiles) {
|
|
268
|
+
const baseDirectory = dirname(settingsFile);
|
|
269
|
+
for (const rawSource of readPackageSources(settingsFile)) {
|
|
270
|
+
const source = rawSource.trim();
|
|
271
|
+
if (!source) continue;
|
|
272
|
+
const packageName = packageNameForSource(source, baseDirectory);
|
|
273
|
+
if (!packageName) continue;
|
|
274
|
+
packages.push({ packageName, source, identity: sourceIdentity(source, baseDirectory) });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return packages;
|
|
278
|
+
}
|
|
279
|
+
function extensionSettingsFiles(cwd) {
|
|
280
|
+
return [join(getAgentDir(), "settings.json"), join(cwd, ".pi", "settings.json")].filter(
|
|
281
|
+
(file) => existsSync(file)
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
function findDuplicateExtensions(installedPackages) {
|
|
285
|
+
const sourcesByPackage = /* @__PURE__ */ new Map();
|
|
286
|
+
for (const extensionPackage of installedPackages) {
|
|
287
|
+
const sources = sourcesByPackage.get(extensionPackage.packageName) ?? /* @__PURE__ */ new Set();
|
|
288
|
+
sources.add(extensionPackage.identity);
|
|
289
|
+
sourcesByPackage.set(extensionPackage.packageName, sources);
|
|
290
|
+
}
|
|
291
|
+
return [...sourcesByPackage.entries()].filter(([, sources]) => sources.size > 1).map(([packageName]) => packageName.replace(/^@[^/]+\//, "").replace(/^pi-/, ""));
|
|
292
|
+
}
|
|
293
|
+
function buildExtensionStatusIconAliases(installedPackages) {
|
|
294
|
+
const packageAliasesByStatusBase = /* @__PURE__ */ new Map();
|
|
295
|
+
for (const extensionPackage of installedPackages) {
|
|
296
|
+
const candidate = extensionStatusIconAliasCandidate(
|
|
297
|
+
extensionPackage.packageName,
|
|
298
|
+
extensionPackage.source
|
|
299
|
+
);
|
|
300
|
+
if (!candidate) continue;
|
|
301
|
+
const aliasesByPackage = packageAliasesByStatusBase.get(candidate.statusBase) ?? /* @__PURE__ */ new Map();
|
|
302
|
+
const existingAliases = aliasesByPackage.get(extensionPackage.packageName) ?? [];
|
|
303
|
+
aliasesByPackage.set(
|
|
304
|
+
extensionPackage.packageName,
|
|
305
|
+
uniqueStrings([...existingAliases, ...candidate.aliases])
|
|
306
|
+
);
|
|
307
|
+
packageAliasesByStatusBase.set(candidate.statusBase, aliasesByPackage);
|
|
308
|
+
}
|
|
309
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
310
|
+
for (const [statusBase, aliasesByPackage] of packageAliasesByStatusBase) {
|
|
311
|
+
if (aliasesByPackage.size === 1)
|
|
312
|
+
aliases.set(statusBase, [...aliasesByPackage.values()][0] ?? []);
|
|
313
|
+
}
|
|
314
|
+
return aliases;
|
|
315
|
+
}
|
|
316
|
+
function extensionStatusIconAliasCandidate(packageName, source) {
|
|
317
|
+
const packageBase = packageBaseName(packageName);
|
|
318
|
+
const statusBase = statusBaseFromPackageBase(packageBase);
|
|
319
|
+
if (!statusBase) return void 0;
|
|
320
|
+
const sourceAliases = source?.startsWith("npm:") ? [source, `npm:${npmPackageName(source)}`] : [];
|
|
321
|
+
return {
|
|
322
|
+
statusBase,
|
|
323
|
+
aliases: uniqueStrings([...sourceAliases, packageName, packageBase, statusBase])
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function packageBaseName(packageName) {
|
|
327
|
+
const slashIndex = packageName.lastIndexOf("/");
|
|
328
|
+
return slashIndex === -1 ? packageName : packageName.slice(slashIndex + 1);
|
|
329
|
+
}
|
|
330
|
+
function statusBaseFromPackageBase(packageBase) {
|
|
331
|
+
return packageBase.startsWith("pi-") && packageBase.length > "pi-".length ? packageBase.slice("pi-".length) : packageBase;
|
|
332
|
+
}
|
|
333
|
+
function uniqueStrings(values) {
|
|
334
|
+
return [...new Set(values.filter((value) => value.length > 0))];
|
|
335
|
+
}
|
|
336
|
+
function readPackageSources(settingsFile) {
|
|
337
|
+
try {
|
|
338
|
+
const settings = JSON.parse(readFileSync(settingsFile, "utf8"));
|
|
339
|
+
return (settings.packages ?? []).map((entry) => {
|
|
340
|
+
if (typeof entry === "string") return entry;
|
|
341
|
+
if (entry && typeof entry === "object" && typeof entry.source === "string") {
|
|
342
|
+
return entry.source;
|
|
343
|
+
}
|
|
344
|
+
return void 0;
|
|
345
|
+
}).filter((source) => source !== void 0);
|
|
346
|
+
} catch {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function packageNameForSource(source, baseDirectory) {
|
|
351
|
+
if (source.startsWith("npm:")) return npmPackageName(source);
|
|
352
|
+
const packageJson = join(resolveSourcePath(source, baseDirectory), "package.json");
|
|
353
|
+
try {
|
|
354
|
+
const packageData = JSON.parse(readFileSync(packageJson, "utf8"));
|
|
355
|
+
return typeof packageData.name === "string" ? packageData.name : void 0;
|
|
356
|
+
} catch {
|
|
357
|
+
return void 0;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function npmPackageName(source) {
|
|
361
|
+
const spec = source.slice("npm:".length);
|
|
362
|
+
if (spec.startsWith("@")) return spec.split("@").slice(0, 2).join("@");
|
|
363
|
+
return spec.split("@")[0] ?? spec;
|
|
364
|
+
}
|
|
365
|
+
function sourceIdentity(source, baseDirectory) {
|
|
366
|
+
if (source.startsWith("npm:")) return `npm:${npmPackageName(source)}`;
|
|
367
|
+
return resolveSourcePath(source, baseDirectory);
|
|
368
|
+
}
|
|
369
|
+
function resolveSourcePath(source, baseDirectory) {
|
|
370
|
+
return isAbsolute(source) ? source : resolve(baseDirectory, source);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/git-status.ts
|
|
374
|
+
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
375
|
+
var GIT_STATUS_TIMEOUT_MS = 3e3;
|
|
376
|
+
async function readGitStatus(pi, cwd, signal) {
|
|
377
|
+
const [statusResult, rootResult] = await Promise.all([
|
|
378
|
+
pi.exec(
|
|
379
|
+
"git",
|
|
380
|
+
["--no-optional-locks", "status", "--porcelain=v1", "--branch", "--untracked-files=normal"],
|
|
381
|
+
{ cwd, timeout: GIT_STATUS_TIMEOUT_MS, signal }
|
|
382
|
+
),
|
|
383
|
+
pi.exec("git", ["rev-parse", "--path-format=absolute", "--show-toplevel"], {
|
|
384
|
+
cwd,
|
|
385
|
+
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
386
|
+
signal
|
|
387
|
+
}).catch(() => void 0)
|
|
388
|
+
]);
|
|
389
|
+
if (statusResult.code !== 0 || statusResult.killed) return void 0;
|
|
390
|
+
const summary = parseGitStatusPorcelain(statusResult.stdout);
|
|
391
|
+
if (rootResult && rootResult.code === 0 && !rootResult.killed) {
|
|
392
|
+
const root = parseGitRoot(rootResult.stdout);
|
|
393
|
+
if (root) summary.root = root;
|
|
394
|
+
}
|
|
395
|
+
return summary;
|
|
396
|
+
}
|
|
397
|
+
function parseGitStatusPorcelain(output) {
|
|
398
|
+
const summary = {
|
|
399
|
+
ahead: 0,
|
|
400
|
+
behind: 0,
|
|
401
|
+
staged: 0,
|
|
402
|
+
modified: 0,
|
|
403
|
+
untracked: 0,
|
|
404
|
+
conflicts: 0
|
|
405
|
+
};
|
|
406
|
+
for (const line of output.split(/\r?\n/)) {
|
|
407
|
+
if (!line) continue;
|
|
408
|
+
if (line.startsWith("## ")) {
|
|
409
|
+
const ahead = line.match(/\bahead (\d+)/u);
|
|
410
|
+
const behind = line.match(/\bbehind (\d+)/u);
|
|
411
|
+
summary.ahead = ahead ? Number(ahead[1]) : 0;
|
|
412
|
+
summary.behind = behind ? Number(behind[1]) : 0;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const indexStatus = line[0] ?? " ";
|
|
416
|
+
const worktreeStatus = line[1] ?? " ";
|
|
417
|
+
if (indexStatus === "?" && worktreeStatus === "?") {
|
|
418
|
+
summary.untracked += 1;
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (isConflictStatus(indexStatus, worktreeStatus)) {
|
|
422
|
+
summary.conflicts += 1;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (isChangedStatus(indexStatus)) summary.staged += 1;
|
|
426
|
+
if (isChangedStatus(worktreeStatus)) summary.modified += 1;
|
|
427
|
+
}
|
|
428
|
+
return summary;
|
|
429
|
+
}
|
|
430
|
+
function isConflictStatus(indexStatus, worktreeStatus) {
|
|
431
|
+
return indexStatus === "D" && worktreeStatus === "D" || indexStatus === "A" && worktreeStatus === "A" || indexStatus === "U" || worktreeStatus === "U";
|
|
432
|
+
}
|
|
433
|
+
function isChangedStatus(status) {
|
|
434
|
+
return status !== " " && status !== "?" && status !== "!";
|
|
435
|
+
}
|
|
436
|
+
function parseGitRoot(output) {
|
|
437
|
+
const root = output.split(/\r?\n/u)[0]?.trim();
|
|
438
|
+
return root && isAbsolute2(root) ? root : void 0;
|
|
439
|
+
}
|
|
440
|
+
function formatGitStatusSummary(summary) {
|
|
441
|
+
if (!summary) return "";
|
|
442
|
+
const tokens = [
|
|
443
|
+
["\u21E1", summary.ahead],
|
|
444
|
+
["\u21E3", summary.behind],
|
|
445
|
+
["+", summary.staged],
|
|
446
|
+
["~", summary.modified],
|
|
447
|
+
["?", summary.untracked],
|
|
448
|
+
["!", summary.conflicts]
|
|
449
|
+
];
|
|
450
|
+
return tokens.filter(([, count]) => count > 0).map(([prefix, count]) => `${prefix}${formatCount(count)}`).join(" ");
|
|
451
|
+
}
|
|
452
|
+
function formatGitBranchValue(branch, status, pr) {
|
|
453
|
+
if (!branch) return "no-git";
|
|
454
|
+
const suffixes = [formatGitStatusSummary(status), pr ? `(${pr})` : ""].filter(Boolean);
|
|
455
|
+
return suffixes.length > 0 ? `${branch} ${suffixes.join(" ")}` : branch;
|
|
456
|
+
}
|
|
457
|
+
function gitStatusSummaryEqual(left, right) {
|
|
458
|
+
if (!left || !right) return left === right;
|
|
459
|
+
return left.root === right.root && left.ahead === right.ahead && left.behind === right.behind && left.staged === right.staged && left.modified === right.modified && left.untracked === right.untracked && left.conflicts === right.conflicts;
|
|
460
|
+
}
|
|
461
|
+
function formatCount(value) {
|
|
462
|
+
return Intl.NumberFormat("en", { notation: "compact", maximumFractionDigits: 1 }).format(value);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// src/render.ts
|
|
466
|
+
import { sanitizeTerminalText as sanitizeTerminalText2 } from "@narumitw/pi-tui-kit/terminal-text";
|
|
467
|
+
|
|
468
|
+
// src/directory.ts
|
|
469
|
+
import { basename, isAbsolute as isAbsolute3, relative, sep } from "node:path";
|
|
470
|
+
import { sanitizeTerminalText } from "@narumitw/pi-tui-kit/terminal-text";
|
|
471
|
+
var DEFAULT_TRUNCATION_LENGTH = 3;
|
|
472
|
+
var DEFAULT_HOME_SYMBOL = "~";
|
|
473
|
+
function formatDirectoryPath(cwd, homeDir, gitRoot) {
|
|
474
|
+
const contracted = gitRoot && (!homeDir || !samePath(gitRoot, homeDir)) ? contractRepositoryPath(cwd, gitRoot) ?? contractPath(cwd, homeDir, DEFAULT_HOME_SYMBOL) : contractPath(cwd, homeDir, DEFAULT_HOME_SYMBOL);
|
|
475
|
+
const safeContracted = sanitizeTerminalText(contracted);
|
|
476
|
+
const components = safeContracted.split("/").filter((component) => component.length > 0);
|
|
477
|
+
const displayed = components.length > DEFAULT_TRUNCATION_LENGTH ? components.slice(-DEFAULT_TRUNCATION_LENGTH).join("/") : safeContracted;
|
|
478
|
+
const native = sep === "/" ? displayed : displayed.replaceAll("/", sep);
|
|
479
|
+
return sanitizeTerminalText(native || basename(cwd) || cwd);
|
|
480
|
+
}
|
|
481
|
+
function toSlashPath(value) {
|
|
482
|
+
return sep === "\\" ? value.replaceAll("\\", "/") : value;
|
|
483
|
+
}
|
|
484
|
+
function contractPath(path, root, replacement) {
|
|
485
|
+
if (!root) return toSlashPath(path);
|
|
486
|
+
const child = relativeWithin(path, root);
|
|
487
|
+
if (child === void 0) return toSlashPath(path);
|
|
488
|
+
return child ? `${replacement}/${child}` : replacement;
|
|
489
|
+
}
|
|
490
|
+
function contractRepositoryPath(path, root) {
|
|
491
|
+
const child = relativeWithin(path, root);
|
|
492
|
+
if (child === void 0) return void 0;
|
|
493
|
+
const name = basename(root) || toSlashPath(root);
|
|
494
|
+
return child ? `${name}/${child}` : name;
|
|
495
|
+
}
|
|
496
|
+
function relativeWithin(path, root) {
|
|
497
|
+
const child = relative(root, path);
|
|
498
|
+
if (child === "") return "";
|
|
499
|
+
if (child === ".." || child.startsWith(`..${sep}`) || isAbsolute3(child)) return void 0;
|
|
500
|
+
return toSlashPath(child);
|
|
501
|
+
}
|
|
502
|
+
function samePath(left, right) {
|
|
503
|
+
return relative(left, right) === "";
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/usage.ts
|
|
507
|
+
function summarizeFooterUsage(entries) {
|
|
508
|
+
const totals = {
|
|
509
|
+
input: 0,
|
|
510
|
+
output: 0,
|
|
511
|
+
cacheRead: 0,
|
|
512
|
+
cacheWrite: 0,
|
|
513
|
+
cost: 0
|
|
514
|
+
};
|
|
515
|
+
for (const entry of entries) {
|
|
516
|
+
let usage;
|
|
517
|
+
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
518
|
+
usage = entry.message.usage;
|
|
519
|
+
const input = usage.input ?? 0;
|
|
520
|
+
const cacheRead = usage.cacheRead ?? 0;
|
|
521
|
+
const cacheWrite = usage.cacheWrite ?? 0;
|
|
522
|
+
const promptTokens = input + cacheRead + cacheWrite;
|
|
523
|
+
totals.latestCacheHitRate = promptTokens > 0 ? cacheRead / promptTokens * 100 : void 0;
|
|
524
|
+
} else if (entry.type === "message" && entry.message.role === "toolResult") {
|
|
525
|
+
usage = entry.message.usage;
|
|
526
|
+
} else if (entry.type === "compaction" || entry.type === "branch_summary") {
|
|
527
|
+
usage = entry.usage;
|
|
528
|
+
}
|
|
529
|
+
if (!usage) continue;
|
|
530
|
+
totals.input += usage.input ?? 0;
|
|
531
|
+
totals.output += usage.output ?? 0;
|
|
532
|
+
totals.cacheRead += usage.cacheRead ?? 0;
|
|
533
|
+
totals.cacheWrite += usage.cacheWrite ?? 0;
|
|
534
|
+
totals.cost += usage.cost?.total ?? 0;
|
|
535
|
+
}
|
|
536
|
+
return totals;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// src/render.ts
|
|
540
|
+
var GITHUB_PR_KEY = "github-pr";
|
|
541
|
+
var GITHUB_PR_STATUS_KEYS = /* @__PURE__ */ new Set([GITHUB_PR_KEY]);
|
|
542
|
+
function renderStatusline(width, ctx, footerData, _theme, config, runtime) {
|
|
543
|
+
if (width <= 0) return "";
|
|
544
|
+
const usageSummary = summarizeFooterUsage(ctx.sessionManager.getEntries());
|
|
545
|
+
const rows = [
|
|
546
|
+
{ configuredSegments: 0, segments: [] }
|
|
547
|
+
];
|
|
548
|
+
for (const name of config.segments) {
|
|
549
|
+
if (name === LINE_BREAK_SEGMENT_NAME) {
|
|
550
|
+
rows.push({ configuredSegments: 0, segments: [] });
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
const row = rows.at(-1);
|
|
554
|
+
if (!row) continue;
|
|
555
|
+
row.configuredSegments += 1;
|
|
556
|
+
const rendered = buildSegment(name, ctx, footerData, config, runtime, usageSummary);
|
|
557
|
+
if (rendered && rendered.text.length > 0) row.segments.push(rendered);
|
|
558
|
+
}
|
|
559
|
+
const segments = [];
|
|
560
|
+
const renderedRows = rows.filter(
|
|
561
|
+
(row) => row.configuredSegments === 0 || row.segments.length > 0
|
|
562
|
+
);
|
|
563
|
+
for (const [index, row] of renderedRows.entries()) {
|
|
564
|
+
if (index > 0) segments.push({ name: LINE_BREAK_SEGMENT_NAME });
|
|
565
|
+
segments.push(...row.segments);
|
|
566
|
+
}
|
|
567
|
+
return renderPowerlineStatusline(width, segments, config);
|
|
568
|
+
}
|
|
569
|
+
function renderExtensionStatusline(width, footerData, theme, config, runtime, mainLine) {
|
|
570
|
+
const statuses = footerData.getExtensionStatuses();
|
|
571
|
+
const prContext = prContextFromStatuses(statuses);
|
|
572
|
+
const rendersPrInline = prContext !== void 0 && mainLine.includes(prContext);
|
|
573
|
+
const status = formatExtensionStatuses(
|
|
574
|
+
statuses,
|
|
575
|
+
theme,
|
|
576
|
+
config,
|
|
577
|
+
runtime,
|
|
578
|
+
rendersPrInline ? GITHUB_PR_STATUS_KEYS : void 0
|
|
579
|
+
);
|
|
580
|
+
return wrapExtensionStatusline(status, width);
|
|
581
|
+
}
|
|
582
|
+
function buildSegment(name, ctx, footerData, config, runtime, usageSummary) {
|
|
583
|
+
switch (name) {
|
|
584
|
+
case "brand":
|
|
585
|
+
return segment(name, "\u03C0", config, "accent", "header", true);
|
|
586
|
+
case "provider":
|
|
587
|
+
return segment(name, ctx.model?.provider ?? "no-provider", config, "accent", "header");
|
|
588
|
+
case "model": {
|
|
589
|
+
const presentation = config.segmentText.model;
|
|
590
|
+
const model = truncateModel(
|
|
591
|
+
shortenModel(ctx.model?.id ?? "no-model"),
|
|
592
|
+
presentation.truncationLength,
|
|
593
|
+
presentation.truncationSymbol,
|
|
594
|
+
presentation.truncationDirection
|
|
595
|
+
);
|
|
596
|
+
return segment(name, model, config, "accent", "header");
|
|
597
|
+
}
|
|
598
|
+
case "thinking":
|
|
599
|
+
return segment(
|
|
600
|
+
name,
|
|
601
|
+
runtime.thinkingLevel,
|
|
602
|
+
config,
|
|
603
|
+
thinkingColor(runtime.thinkingLevel),
|
|
604
|
+
"header"
|
|
605
|
+
);
|
|
606
|
+
case "branch": {
|
|
607
|
+
const branch = footerData.getGitBranch();
|
|
608
|
+
const pr = branch ? prContextFromStatuses(footerData.getExtensionStatuses()) : void 0;
|
|
609
|
+
return segment(
|
|
610
|
+
name,
|
|
611
|
+
formatGitBranchValue(branch, runtime.gitStatus, pr),
|
|
612
|
+
config,
|
|
613
|
+
"accent",
|
|
614
|
+
"git"
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
case "cwd":
|
|
618
|
+
return segment(
|
|
619
|
+
name,
|
|
620
|
+
formatDirectoryPath(ctx.cwd, runtime.homeDir, runtime.gitStatus?.root),
|
|
621
|
+
config,
|
|
622
|
+
"accent",
|
|
623
|
+
"directory"
|
|
624
|
+
);
|
|
625
|
+
case "tools": {
|
|
626
|
+
const activity = formatToolActivity(runtime);
|
|
627
|
+
return activity ? segment(name, activity, config, "accent", "runtime") : void 0;
|
|
628
|
+
}
|
|
629
|
+
case "context": {
|
|
630
|
+
const usage = ctx.getContextUsage();
|
|
631
|
+
const percentage = usage?.percent === null || usage?.percent === void 0 ? "?" : `${usage.percent.toFixed(1)}%`;
|
|
632
|
+
const contextWindow = usage?.contextWindow ?? ctx.model?.contextWindow ?? 0;
|
|
633
|
+
return segment(
|
|
634
|
+
name,
|
|
635
|
+
`${percentage}/${formatCount2(contextWindow)}`,
|
|
636
|
+
config,
|
|
637
|
+
contextColor(usage?.percent),
|
|
638
|
+
"runtime"
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
case "tokens": {
|
|
642
|
+
const value = usageSummary.input === 0 && usageSummary.output === 0 ? "tok 0" : `\u2191${formatCount2(usageSummary.input)} \u2193${formatCount2(usageSummary.output)}`;
|
|
643
|
+
return segment(name, value, config, "accent", "runtime");
|
|
644
|
+
}
|
|
645
|
+
case "cache": {
|
|
646
|
+
if (usageSummary.cacheRead === 0 && usageSummary.cacheWrite === 0) return void 0;
|
|
647
|
+
const values = [];
|
|
648
|
+
if (usageSummary.cacheRead > 0) values.push(`R${formatCount2(usageSummary.cacheRead)}`);
|
|
649
|
+
if (usageSummary.cacheWrite > 0) values.push(`W${formatCount2(usageSummary.cacheWrite)}`);
|
|
650
|
+
if (usageSummary.latestCacheHitRate !== void 0) {
|
|
651
|
+
values.push(`CH${usageSummary.latestCacheHitRate.toFixed(1)}%`);
|
|
652
|
+
}
|
|
653
|
+
return segment(name, values.join(" "), config, "accent", "runtime");
|
|
654
|
+
}
|
|
655
|
+
case "cost": {
|
|
656
|
+
const subscription = isSubscriptionBacked(ctx) ? " (sub)" : "";
|
|
657
|
+
return segment(
|
|
658
|
+
name,
|
|
659
|
+
`${usageSummary.cost.toFixed(usageSummary.cost >= 1 ? 2 : 3)}${subscription}`,
|
|
660
|
+
config,
|
|
661
|
+
"accent",
|
|
662
|
+
"meter"
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
case "time":
|
|
666
|
+
return segment(name, formatTime(), config, "accent", "meter");
|
|
667
|
+
case "turn":
|
|
668
|
+
return segment(name, `${runtime.turnCount}`, config, "accent", "meter");
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
function segment(name, value, config, color, block, emphasis = false) {
|
|
672
|
+
return { name, text: formatConfiguredSegment(name, value, config), color, block, emphasis };
|
|
673
|
+
}
|
|
674
|
+
function formatConfiguredSegment(name, value, config) {
|
|
675
|
+
const presentation = config.segmentText[name];
|
|
676
|
+
return `${presentation.prefix}${value}${presentation.suffix}`;
|
|
677
|
+
}
|
|
678
|
+
function thinkingColor(level) {
|
|
679
|
+
switch (level) {
|
|
680
|
+
case "off":
|
|
681
|
+
return "dim";
|
|
682
|
+
case "minimal":
|
|
683
|
+
return "thinkingMinimal";
|
|
684
|
+
case "low":
|
|
685
|
+
return "thinkingLow";
|
|
686
|
+
case "medium":
|
|
687
|
+
return "thinkingMedium";
|
|
688
|
+
case "high":
|
|
689
|
+
return "thinkingHigh";
|
|
690
|
+
case "xhigh":
|
|
691
|
+
return "thinkingXhigh";
|
|
692
|
+
case "max":
|
|
693
|
+
return "thinkingMax";
|
|
694
|
+
default:
|
|
695
|
+
return "dim";
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
function contextColor(percent) {
|
|
699
|
+
if (percent === null || percent === void 0) return "dim";
|
|
700
|
+
if (percent >= 90) return "error";
|
|
701
|
+
if (percent >= 70) return "warning";
|
|
702
|
+
return "success";
|
|
703
|
+
}
|
|
704
|
+
function formatToolActivity(runtime) {
|
|
705
|
+
const active = [...runtime.activeTools.entries()];
|
|
706
|
+
if (active.length > 0) {
|
|
707
|
+
const [name, count] = active[0] ?? ["tool", 1];
|
|
708
|
+
const suffix = count > 1 ? `\xD7${count}` : active.length > 1 ? `+${active.length - 1}` : "";
|
|
709
|
+
return `\u2699 ${name}${suffix}`;
|
|
710
|
+
}
|
|
711
|
+
return runtime.isStreaming ? "\u{1F4AD} thinking" : void 0;
|
|
712
|
+
}
|
|
713
|
+
function prLinkFromStatuses(statuses) {
|
|
714
|
+
const value = statuses.get(GITHUB_PR_KEY);
|
|
715
|
+
if (!value) return void 0;
|
|
716
|
+
const open = value.indexOf("\x1B]8;;");
|
|
717
|
+
if (open === -1) return void 0;
|
|
718
|
+
const closeMarker = "\x1B]8;;\x07";
|
|
719
|
+
const close = value.indexOf(closeMarker, open + 1);
|
|
720
|
+
return close === -1 ? void 0 : value.slice(open, close + closeMarker.length);
|
|
721
|
+
}
|
|
722
|
+
function prContextFromStatuses(statuses) {
|
|
723
|
+
const value = statuses.get(GITHUB_PR_KEY);
|
|
724
|
+
const link = prLinkFromStatuses(statuses);
|
|
725
|
+
if (!value || !link) return void 0;
|
|
726
|
+
const state = compactPrState(value.replace(link, ""));
|
|
727
|
+
return state ? `${link} \xB7 ${state}` : void 0;
|
|
728
|
+
}
|
|
729
|
+
function compactPrState(value) {
|
|
730
|
+
if (/:\s*merged\s*$/.test(value)) return "merged";
|
|
731
|
+
if (/:\s*closed\s*$/.test(value)) return "closed";
|
|
732
|
+
if (/\bdraft\b/.test(value)) return "draft";
|
|
733
|
+
const failing = /\bchecks failing \((\d+)\)/.exec(value);
|
|
734
|
+
if (failing) return `${failing[1]} failing`;
|
|
735
|
+
if (/\bchanges requested\b/.test(value)) return "changes requested";
|
|
736
|
+
const pending = /\bchecks pending \((\d+)\)/.exec(value);
|
|
737
|
+
if (pending) return `${pending[1]} pending`;
|
|
738
|
+
if (/\bapproved\b/.test(value)) return "approved";
|
|
739
|
+
if (/\breview required\b/.test(value)) return "review required";
|
|
740
|
+
if (/\bchecks passing\b/.test(value)) return "checks passing";
|
|
741
|
+
if (/\bno checks\b/.test(value)) return "no checks";
|
|
742
|
+
return void 0;
|
|
743
|
+
}
|
|
744
|
+
function isSubscriptionBacked(ctx) {
|
|
745
|
+
const model = ctx.model;
|
|
746
|
+
return model !== void 0 && (model.provider === "kimi-coding" || ctx.modelRegistry.isUsingOAuth(model));
|
|
747
|
+
}
|
|
748
|
+
function formatCount2(value) {
|
|
749
|
+
if (value < 1e3) return `${value}`;
|
|
750
|
+
if (value < 1e6) return `${(value / 1e3).toFixed(value < 1e4 ? 1 : 0)}k`;
|
|
751
|
+
return `${(value / 1e6).toFixed(1)}m`;
|
|
752
|
+
}
|
|
753
|
+
function formatTime() {
|
|
754
|
+
const now = /* @__PURE__ */ new Date();
|
|
755
|
+
const hours = now.getHours().toString().padStart(2, "0");
|
|
756
|
+
const minutes = now.getMinutes().toString().padStart(2, "0");
|
|
757
|
+
return `${hours}:${minutes}`;
|
|
758
|
+
}
|
|
759
|
+
var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
760
|
+
function truncateModel(model, length, symbol, direction) {
|
|
761
|
+
const safeModel = sanitizeTerminalText2(model);
|
|
762
|
+
if (length === 0) return safeModel;
|
|
763
|
+
const graphemes = [...graphemeSegmenter.segment(safeModel)].map(({ segment: segment2 }) => segment2);
|
|
764
|
+
if (graphemes.length <= length) return safeModel;
|
|
765
|
+
const safeSymbol = sanitizeTerminalText2(symbol);
|
|
766
|
+
switch (direction) {
|
|
767
|
+
case "start":
|
|
768
|
+
return `${safeSymbol}${graphemes.slice(-length).join("")}`;
|
|
769
|
+
case "middle": {
|
|
770
|
+
const headLength = Math.ceil(length / 2);
|
|
771
|
+
const tailLength = Math.floor(length / 2);
|
|
772
|
+
const tail = tailLength > 0 ? graphemes.slice(-tailLength).join("") : "";
|
|
773
|
+
return `${graphemes.slice(0, headLength).join("")}${safeSymbol}${tail}`;
|
|
774
|
+
}
|
|
775
|
+
case "end":
|
|
776
|
+
return `${graphemes.slice(0, length).join("")}${safeSymbol}`;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
function shortenModel(model) {
|
|
780
|
+
return model.replace(/^claude-/, "").replace(/^gpt-/, "gpt ").replace(/-20\d{6}$/, "").replace(/-latest$/, "");
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/statusline.ts
|
|
784
|
+
var STATUSLINE_KEY2 = "statusline";
|
|
785
|
+
var GIT_STATUS_REFRESH_INTERVAL_MS = 3e4;
|
|
786
|
+
var GIT_STATUS_EVENT_DEBOUNCE_MS = 250;
|
|
787
|
+
var EMPTY_EXTENSION_STATUS_ICON_ALIASES2 = /* @__PURE__ */ new Map();
|
|
788
|
+
var statuslineCommandsPromise;
|
|
789
|
+
function loadStatuslineCommands() {
|
|
790
|
+
if (!statuslineCommandsPromise) {
|
|
791
|
+
statuslineCommandsPromise = import("./chunks/commands-5CMKUNTM.js").catch((error) => {
|
|
792
|
+
statuslineCommandsPromise = void 0;
|
|
793
|
+
throw error;
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
return statuslineCommandsPromise;
|
|
797
|
+
}
|
|
798
|
+
function statusline(pi) {
|
|
799
|
+
let loaded;
|
|
800
|
+
let previewPalettePreset;
|
|
801
|
+
let activeSessionManager;
|
|
802
|
+
const runtime = {
|
|
803
|
+
turnCount: 0,
|
|
804
|
+
activeTools: /* @__PURE__ */ new Map(),
|
|
805
|
+
isStreaming: false,
|
|
806
|
+
thinkingLevel: "off",
|
|
807
|
+
duplicateExtensions: [],
|
|
808
|
+
extensionStatusIconAliases: EMPTY_EXTENSION_STATUS_ICON_ALIASES2
|
|
809
|
+
};
|
|
810
|
+
let sessionGeneration = 0;
|
|
811
|
+
let menuController = new AbortController();
|
|
812
|
+
let gitStatusRequestId = 0;
|
|
813
|
+
let activeGitStatusTarget;
|
|
814
|
+
let gitStatusRefreshInFlight = false;
|
|
815
|
+
let gitStatusAbortController;
|
|
816
|
+
let gitStatusDebounceTimer;
|
|
817
|
+
let pendingGitStatusRefresh;
|
|
818
|
+
const refresh = () => runtime.requestRender?.();
|
|
819
|
+
const ownsRuntime = (ctx) => ctx.sessionManager === activeSessionManager;
|
|
820
|
+
const setGitStatus = (summary) => {
|
|
821
|
+
if (gitStatusSummaryEqual(runtime.gitStatus, summary)) return;
|
|
822
|
+
runtime.gitStatus = summary;
|
|
823
|
+
refresh();
|
|
824
|
+
};
|
|
825
|
+
const clearGitStatusDebounce = () => {
|
|
826
|
+
if (!gitStatusDebounceTimer) return;
|
|
827
|
+
clearTimeout(gitStatusDebounceTimer);
|
|
828
|
+
gitStatusDebounceTimer = void 0;
|
|
829
|
+
};
|
|
830
|
+
const abortGitStatusRefresh = (reason) => {
|
|
831
|
+
gitStatusAbortController?.abort(new DOMException(reason, "AbortError"));
|
|
832
|
+
};
|
|
833
|
+
const isActiveGitStatusTarget = (cwd, generation) => activeGitStatusTarget?.cwd === cwd && activeGitStatusTarget.generation === generation && generation === sessionGeneration;
|
|
834
|
+
const isCurrentGitStatusRequest = (cwd, generation, requestId) => isActiveGitStatusTarget(cwd, generation) && requestId === gitStatusRequestId;
|
|
835
|
+
const runGitStatusRefresh = (cwd, generation, requestId) => {
|
|
836
|
+
if (!isCurrentGitStatusRequest(cwd, generation, requestId)) return;
|
|
837
|
+
if (gitStatusRefreshInFlight) {
|
|
838
|
+
pendingGitStatusRefresh = { cwd, generation, requestId };
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
gitStatusRefreshInFlight = true;
|
|
842
|
+
const abortController = new AbortController();
|
|
843
|
+
gitStatusAbortController = abortController;
|
|
844
|
+
void (async () => {
|
|
845
|
+
try {
|
|
846
|
+
const summary = await readGitStatus(pi, cwd, abortController.signal);
|
|
847
|
+
if (isCurrentGitStatusRequest(cwd, generation, requestId)) setGitStatus(summary);
|
|
848
|
+
} catch {
|
|
849
|
+
if (isCurrentGitStatusRequest(cwd, generation, requestId)) setGitStatus(void 0);
|
|
850
|
+
} finally {
|
|
851
|
+
gitStatusRefreshInFlight = false;
|
|
852
|
+
if (gitStatusAbortController === abortController) gitStatusAbortController = void 0;
|
|
853
|
+
const pending = pendingGitStatusRefresh;
|
|
854
|
+
pendingGitStatusRefresh = void 0;
|
|
855
|
+
if (pending) runGitStatusRefresh(pending.cwd, pending.generation, pending.requestId);
|
|
856
|
+
}
|
|
857
|
+
})();
|
|
858
|
+
};
|
|
859
|
+
const refreshGitStatus = (cwd, generation = sessionGeneration) => {
|
|
860
|
+
if (!isActiveGitStatusTarget(cwd, generation)) return;
|
|
861
|
+
runGitStatusRefresh(cwd, generation, ++gitStatusRequestId);
|
|
862
|
+
};
|
|
863
|
+
const scheduleGitStatusRefresh = (cwd, generation = sessionGeneration) => {
|
|
864
|
+
if (!isActiveGitStatusTarget(cwd, generation)) return;
|
|
865
|
+
const requestId = ++gitStatusRequestId;
|
|
866
|
+
clearGitStatusDebounce();
|
|
867
|
+
gitStatusDebounceTimer = setTimeout(() => {
|
|
868
|
+
gitStatusDebounceTimer = void 0;
|
|
869
|
+
runGitStatusRefresh(cwd, generation, requestId);
|
|
870
|
+
}, GIT_STATUS_EVENT_DEBOUNCE_MS);
|
|
871
|
+
};
|
|
872
|
+
const scheduleGitStatusRefreshForContext = (ctx) => {
|
|
873
|
+
if (!activeGitStatusTarget || activeGitStatusTarget.cwd !== ctx.cwd) return;
|
|
874
|
+
scheduleGitStatusRefresh(activeGitStatusTarget.cwd, activeGitStatusTarget.generation);
|
|
875
|
+
};
|
|
876
|
+
const installFooter = (ctx) => {
|
|
877
|
+
const generation = ++sessionGeneration;
|
|
878
|
+
menuController.abort(new DOMException("Statusline session context replaced", "AbortError"));
|
|
879
|
+
menuController = new AbortController();
|
|
880
|
+
const cwd = ctx.cwd;
|
|
881
|
+
activeSessionManager = ctx.sessionManager;
|
|
882
|
+
runtime.homeDir = homedir();
|
|
883
|
+
previewPalettePreset = void 0;
|
|
884
|
+
abortGitStatusRefresh("Statusline session context replaced");
|
|
885
|
+
clearGitStatusDebounce();
|
|
886
|
+
activeGitStatusTarget = ctx.mode === "tui" ? { cwd, generation } : void 0;
|
|
887
|
+
runtime.gitStatus = void 0;
|
|
888
|
+
runtime.duplicateExtensions = [];
|
|
889
|
+
runtime.extensionStatusIconAliases = EMPTY_EXTENSION_STATUS_ICON_ALIASES2;
|
|
890
|
+
ctx.ui.setStatus(STATUSLINE_KEY2, void 0);
|
|
891
|
+
if (!activeGitStatusTarget || !loaded) return;
|
|
892
|
+
const installedPackages = readInstalledExtensionPackages(cwd);
|
|
893
|
+
runtime.duplicateExtensions = findDuplicateExtensions(installedPackages);
|
|
894
|
+
runtime.extensionStatusIconAliases = buildExtensionStatusIconAliases(installedPackages);
|
|
895
|
+
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
896
|
+
runtime.requestRender = () => tui.requestRender();
|
|
897
|
+
const refreshFooterGitStatus = () => refreshGitStatus(cwd, generation);
|
|
898
|
+
const branchUnsubscribe = footerData.onBranchChange(() => {
|
|
899
|
+
runtime.gitStatus = void 0;
|
|
900
|
+
abortGitStatusRefresh("Statusline Git branch changed");
|
|
901
|
+
clearGitStatusDebounce();
|
|
902
|
+
refreshFooterGitStatus();
|
|
903
|
+
tui.requestRender();
|
|
904
|
+
});
|
|
905
|
+
const clock = setInterval(() => {
|
|
906
|
+
clearGitStatusDebounce();
|
|
907
|
+
refreshFooterGitStatus();
|
|
908
|
+
tui.requestRender();
|
|
909
|
+
}, GIT_STATUS_REFRESH_INTERVAL_MS);
|
|
910
|
+
return {
|
|
911
|
+
dispose() {
|
|
912
|
+
branchUnsubscribe();
|
|
913
|
+
clearInterval(clock);
|
|
914
|
+
if (isActiveGitStatusTarget(cwd, generation)) {
|
|
915
|
+
activeGitStatusTarget = void 0;
|
|
916
|
+
abortGitStatusRefresh("Statusline footer disposed");
|
|
917
|
+
clearGitStatusDebounce();
|
|
918
|
+
pendingGitStatusRefresh = void 0;
|
|
919
|
+
runtime.gitStatus = void 0;
|
|
920
|
+
runtime.duplicateExtensions = [];
|
|
921
|
+
runtime.extensionStatusIconAliases = EMPTY_EXTENSION_STATUS_ICON_ALIASES2;
|
|
922
|
+
runtime.requestRender = void 0;
|
|
923
|
+
}
|
|
924
|
+
},
|
|
925
|
+
invalidate() {
|
|
926
|
+
},
|
|
927
|
+
render(width) {
|
|
928
|
+
if (!loaded) return [];
|
|
929
|
+
const config = previewPalettePreset ? { ...loaded.config, palettePreset: previewPalettePreset } : loaded.config;
|
|
930
|
+
const mainLine = renderStatusline(width, ctx, footerData, theme, config, runtime);
|
|
931
|
+
const lines = mainLine ? mainLine.split("\n") : [];
|
|
932
|
+
lines.push(
|
|
933
|
+
...renderExtensionStatusline(width, footerData, theme, config, runtime, mainLine)
|
|
934
|
+
);
|
|
935
|
+
return lines;
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
});
|
|
939
|
+
refreshGitStatus(cwd, generation);
|
|
940
|
+
};
|
|
941
|
+
const agentDir = getAgentDir2();
|
|
942
|
+
const configPath = settingsFilePath(agentDir);
|
|
943
|
+
const commandOptions = {
|
|
944
|
+
settingsPath: configPath,
|
|
945
|
+
getLoaded: () => loaded ?? loadStatuslineSettings(configPath),
|
|
946
|
+
getMenuOwner: () => {
|
|
947
|
+
const generation = sessionGeneration;
|
|
948
|
+
return {
|
|
949
|
+
signal: menuController.signal,
|
|
950
|
+
isCurrent: () => generation === sessionGeneration && !menuController.signal.aborted
|
|
951
|
+
};
|
|
952
|
+
},
|
|
953
|
+
apply(next, ctx) {
|
|
954
|
+
if (ctx.sessionManager !== activeSessionManager) return;
|
|
955
|
+
previewPalettePreset = void 0;
|
|
956
|
+
loaded = next;
|
|
957
|
+
refresh();
|
|
958
|
+
},
|
|
959
|
+
preview(palettePreset, ctx) {
|
|
960
|
+
if (ctx.sessionManager !== activeSessionManager) return;
|
|
961
|
+
previewPalettePreset = palettePreset;
|
|
962
|
+
refresh();
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
pi.registerCommand("statusline", {
|
|
966
|
+
description: "Open or inspect the statusline settings",
|
|
967
|
+
getArgumentCompletions: completeStatuslineArguments,
|
|
968
|
+
handler: async (args, ctx) => {
|
|
969
|
+
const commands = await loadStatuslineCommands();
|
|
970
|
+
if (ctx.sessionManager !== activeSessionManager) return;
|
|
971
|
+
await commands.handleStatuslineCommand(args, ctx, commandOptions);
|
|
972
|
+
}
|
|
973
|
+
});
|
|
974
|
+
pi.on("session_start", (_event, ctx) => {
|
|
975
|
+
runtime.turnCount = 0;
|
|
976
|
+
runtime.activeTools.clear();
|
|
977
|
+
runtime.isStreaming = false;
|
|
978
|
+
loaded = loadStatuslineSettingsForAgent(agentDir);
|
|
979
|
+
const settingsNotice = consumeStatuslineSettingsNotice();
|
|
980
|
+
if (settingsNotice) ctx.ui.notify(settingsNotice, "warning");
|
|
981
|
+
if (loaded.diagnostics.length > 0) {
|
|
982
|
+
ctx.ui.notify(formatSettingsDiagnostics(loaded), "warning");
|
|
983
|
+
}
|
|
984
|
+
runtime.thinkingLevel = pi.getThinkingLevel();
|
|
985
|
+
installFooter(ctx);
|
|
986
|
+
});
|
|
987
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
988
|
+
installFooter(ctx);
|
|
989
|
+
refresh();
|
|
990
|
+
});
|
|
991
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
992
|
+
if (!ownsRuntime(ctx)) return;
|
|
993
|
+
sessionGeneration += 1;
|
|
994
|
+
menuController.abort(new DOMException("Statusline session shut down", "AbortError"));
|
|
995
|
+
activeSessionManager = void 0;
|
|
996
|
+
previewPalettePreset = void 0;
|
|
997
|
+
activeGitStatusTarget = void 0;
|
|
998
|
+
abortGitStatusRefresh("Statusline session shut down");
|
|
999
|
+
clearGitStatusDebounce();
|
|
1000
|
+
pendingGitStatusRefresh = void 0;
|
|
1001
|
+
runtime.gitStatus = void 0;
|
|
1002
|
+
runtime.activeTools.clear();
|
|
1003
|
+
runtime.isStreaming = false;
|
|
1004
|
+
runtime.duplicateExtensions = [];
|
|
1005
|
+
runtime.extensionStatusIconAliases = EMPTY_EXTENSION_STATUS_ICON_ALIASES2;
|
|
1006
|
+
ctx.ui.setFooter(void 0);
|
|
1007
|
+
ctx.ui.setStatus(STATUSLINE_KEY2, void 0);
|
|
1008
|
+
runtime.requestRender = void 0;
|
|
1009
|
+
});
|
|
1010
|
+
pi.on("model_select", () => refresh());
|
|
1011
|
+
pi.on("thinking_level_select", (event) => {
|
|
1012
|
+
runtime.thinkingLevel = event.level;
|
|
1013
|
+
refresh();
|
|
1014
|
+
});
|
|
1015
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
1016
|
+
if (!ownsRuntime(ctx)) return;
|
|
1017
|
+
runtime.isStreaming = true;
|
|
1018
|
+
refresh();
|
|
1019
|
+
});
|
|
1020
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
1021
|
+
if (!ownsRuntime(ctx)) return;
|
|
1022
|
+
runtime.activeTools.clear();
|
|
1023
|
+
scheduleGitStatusRefreshForContext(ctx);
|
|
1024
|
+
refresh();
|
|
1025
|
+
});
|
|
1026
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
1027
|
+
if (!ownsRuntime(ctx)) return;
|
|
1028
|
+
runtime.isStreaming = false;
|
|
1029
|
+
runtime.activeTools.clear();
|
|
1030
|
+
refresh();
|
|
1031
|
+
});
|
|
1032
|
+
pi.on("turn_start", (_event, ctx) => {
|
|
1033
|
+
if (!ownsRuntime(ctx)) return;
|
|
1034
|
+
runtime.turnCount += 1;
|
|
1035
|
+
runtime.isStreaming = true;
|
|
1036
|
+
refresh();
|
|
1037
|
+
});
|
|
1038
|
+
pi.on("turn_end", (_event, ctx) => {
|
|
1039
|
+
if (!ownsRuntime(ctx)) return;
|
|
1040
|
+
scheduleGitStatusRefreshForContext(ctx);
|
|
1041
|
+
refresh();
|
|
1042
|
+
});
|
|
1043
|
+
pi.on("tool_execution_start", (event, ctx) => {
|
|
1044
|
+
if (!ownsRuntime(ctx)) return;
|
|
1045
|
+
const currentCount = runtime.activeTools.get(event.toolName) ?? 0;
|
|
1046
|
+
runtime.activeTools.set(event.toolName, currentCount + 1);
|
|
1047
|
+
refresh();
|
|
1048
|
+
});
|
|
1049
|
+
pi.on("tool_execution_end", (event, ctx) => {
|
|
1050
|
+
if (!ownsRuntime(ctx)) return;
|
|
1051
|
+
const currentCount = runtime.activeTools.get(event.toolName) ?? 0;
|
|
1052
|
+
if (currentCount <= 1) runtime.activeTools.delete(event.toolName);
|
|
1053
|
+
else runtime.activeTools.set(event.toolName, currentCount - 1);
|
|
1054
|
+
scheduleGitStatusRefreshForContext(ctx);
|
|
1055
|
+
refresh();
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
function formatSettingsDiagnostics(loaded) {
|
|
1059
|
+
const details = loaded.diagnostics.slice(0, 5).map((item) => item.message);
|
|
1060
|
+
const remaining = loaded.diagnostics.length - details.length;
|
|
1061
|
+
return [
|
|
1062
|
+
`pi-statusline settings: ${details.join("; ")}`,
|
|
1063
|
+
...remaining > 0 ? [`+${remaining} more`] : []
|
|
1064
|
+
].join(" ");
|
|
1065
|
+
}
|
|
1066
|
+
export {
|
|
1067
|
+
statusline as default
|
|
1068
|
+
};
|
|
1069
|
+
//# sourceMappingURL=index.ts.map
|