@narumitw/pi-statusline 0.50.3 → 0.51.0
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/CHANGELOG.md +126 -0
- package/README.md +12 -1
- package/dist/chunks/{chunk-LARLXEUL.ts → chunk-HJJWE7KU.ts} +43 -2
- package/dist/chunks/{chunk-LARLXEUL.ts.map → chunk-HJJWE7KU.ts.map} +3 -3
- package/dist/chunks/{commands-KDWSOSZZ.ts → commands-WV3JHCQ6.ts} +9 -4
- package/dist/chunks/commands-WV3JHCQ6.ts.map +7 -0
- package/dist/index.ts +314 -96
- package/dist/index.ts.map +4 -4
- package/docs/configuration.md +6 -0
- package/docs/extension-status-protocol.md +92 -0
- package/package.json +6 -5
- package/src/commands.ts +6 -1
- package/src/extension-status-metadata.ts +208 -0
- package/src/extension-status.ts +51 -5
- package/src/git-status.ts +7 -2
- package/src/render.ts +15 -3
- package/src/statusline.ts +102 -41
- package/src/usage.ts +17 -4
- package/dist/chunks/commands-KDWSOSZZ.ts.map +0 -7
package/dist/index.ts
CHANGED
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
loadStatuslineSettings,
|
|
9
9
|
loadStatuslineSettingsForAgent,
|
|
10
10
|
resolvePreset,
|
|
11
|
-
settingsFilePath
|
|
12
|
-
|
|
11
|
+
settingsFilePath,
|
|
12
|
+
summarizeFooterUsage
|
|
13
|
+
} from "./chunks/chunk-HJJWE7KU.ts";
|
|
13
14
|
|
|
14
15
|
// src/statusline.ts
|
|
15
16
|
import { homedir } from "node:os";
|
|
@@ -20,7 +21,8 @@ import { getCapabilities } from "@earendil-works/pi-tui";
|
|
|
20
21
|
import { existsSync, readFileSync } from "node:fs";
|
|
21
22
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
22
23
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
23
|
-
import { wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
24
|
+
import { truncateToWidth, visibleWidth as visibleWidth2, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
25
|
+
import { sanitizeTerminalText } from "@narumitw/pi-tui-kit/terminal-text";
|
|
24
26
|
|
|
25
27
|
// src/powerline.ts
|
|
26
28
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
@@ -170,10 +172,14 @@ function extensionStatusSeparator(config, theme, trueColor) {
|
|
|
170
172
|
return powerlineExtensionSeparator(theme, config.palettePreset, trueColor);
|
|
171
173
|
}
|
|
172
174
|
function formatExtensionStatuses(statuses, theme, config, runtime, hiddenKeys = /* @__PURE__ */ new Set(), trueColor = true) {
|
|
175
|
+
runtime.extensionStatusMetadata?.reconcile(statuses);
|
|
173
176
|
const separator = extensionStatusSeparator(config, theme, trueColor);
|
|
174
177
|
const visibleStatuses = [
|
|
175
178
|
...formatDuplicateExtensionStatus(runtime, theme),
|
|
176
|
-
...[...statuses.entries()].filter(([key, value]) => key !== STATUSLINE_KEY && !hiddenKeys.has(key) && value.trim().length > 0).map(([key, value]) =>
|
|
179
|
+
...[...statuses.entries()].filter(([key, value]) => key !== STATUSLINE_KEY && !hiddenKeys.has(key) && value.trim().length > 0).map(([key, value]) => {
|
|
180
|
+
const presentation = runtime.extensionStatusMetadata?.get(key, value);
|
|
181
|
+
return presentation ? formatCompactExtensionStatus(key, value, presentation, theme, config, runtime.extensionStatusIconAliases) : formatExtensionStatus(key, value, theme, config, runtime.extensionStatusIconAliases);
|
|
182
|
+
}).filter((status) => status.length > 0)
|
|
177
183
|
].slice(0, 5);
|
|
178
184
|
return visibleStatuses.join(separator);
|
|
179
185
|
}
|
|
@@ -186,18 +192,41 @@ function formatExtensionStatus(key, value, theme, config, extensionStatusIconAli
|
|
|
186
192
|
const renderedText = theme.fg(textColor, text);
|
|
187
193
|
return icon ? `${theme.fg(color, icon)} ${renderedText}` : renderedText;
|
|
188
194
|
}
|
|
195
|
+
function formatCompactExtensionStatus(key, value, presentation, theme, config, aliases) {
|
|
196
|
+
if (presentation.activity === "idle") return "";
|
|
197
|
+
const status = splitExtensionStatusIcon(
|
|
198
|
+
stripExtensionStatusPrefix(sanitizeTerminalText(key), sanitizeTerminalText(value))
|
|
199
|
+
);
|
|
200
|
+
const icon = sanitizeTerminalText(extensionStatusIcon(key, status.icon, config.extensionStatusIcons, aliases));
|
|
201
|
+
const bodies = {
|
|
202
|
+
checking: "\u2026",
|
|
203
|
+
working: "\u21BB",
|
|
204
|
+
pushing: "\u2191",
|
|
205
|
+
pulling: "\u2193",
|
|
206
|
+
resolving: "AI",
|
|
207
|
+
retry: `retry: ${presentation.reason}`,
|
|
208
|
+
blocked: `! ${presentation.reason}`,
|
|
209
|
+
"local-change": "\u21E1",
|
|
210
|
+
"remote-change": "\u21E3",
|
|
211
|
+
review: "\u21D5"
|
|
212
|
+
};
|
|
213
|
+
const warning = presentation.activity === "blocked" || presentation.activity === "retry";
|
|
214
|
+
const color = warning ? "warning" : extensionColor(key, value);
|
|
215
|
+
const body = theme.fg(warning ? "warning" : "muted", bodies[presentation.activity]);
|
|
216
|
+
return icon ? `${theme.fg(color, icon)} ${body}` : body;
|
|
217
|
+
}
|
|
189
218
|
function extensionStatusIcon(key, leadingIcon, configuredIcons, extensionStatusIconAliases) {
|
|
190
219
|
if (Object.hasOwn(configuredIcons, key)) return configuredIcons[key];
|
|
191
220
|
const namespaceIcon = configuredNamespaceIcon(key, configuredIcons);
|
|
192
221
|
if (namespaceIcon !== void 0) return namespaceIcon;
|
|
193
|
-
const compatibleKey = COMPATIBLE_STATUS_ICON_KEYS[key];
|
|
222
|
+
const compatibleKey = Object.hasOwn(COMPATIBLE_STATUS_ICON_KEYS, key) ? COMPATIBLE_STATUS_ICON_KEYS[key] : void 0;
|
|
194
223
|
if (compatibleKey && Object.hasOwn(configuredIcons, compatibleKey)) {
|
|
195
224
|
return configuredIcons[compatibleKey];
|
|
196
225
|
}
|
|
197
226
|
for (const alias of extensionStatusAliasesForKey(key, extensionStatusIconAliases)) {
|
|
198
227
|
if (Object.hasOwn(configuredIcons, alias)) return configuredIcons[alias];
|
|
199
228
|
}
|
|
200
|
-
return leadingIcon ?? DEFAULT_EXTENSION_STATUS_ICONS[key]
|
|
229
|
+
return leadingIcon ?? (Object.hasOwn(DEFAULT_EXTENSION_STATUS_ICONS, key) ? DEFAULT_EXTENSION_STATUS_ICONS[key] : "\u{1F50C}");
|
|
201
230
|
}
|
|
202
231
|
function configuredNamespaceIcon(key, configuredIcons) {
|
|
203
232
|
let match;
|
|
@@ -220,7 +249,9 @@ function statusKeyMatchesStatusBase(key, statusBase) {
|
|
|
220
249
|
}
|
|
221
250
|
function wrapExtensionStatusline(status, width) {
|
|
222
251
|
if (!status || width <= 0) return [];
|
|
223
|
-
return wrapTextWithAnsi(status, width)
|
|
252
|
+
return wrapTextWithAnsi(status, width).map(
|
|
253
|
+
(line) => visibleWidth2(line) > width ? truncateToWidth(line, width, "") : line
|
|
254
|
+
);
|
|
224
255
|
}
|
|
225
256
|
function formatDuplicateExtensionStatus(runtime, theme) {
|
|
226
257
|
if (runtime.duplicateExtensions.length === 0) return [];
|
|
@@ -357,25 +388,178 @@ function resolveSourcePath(source, baseDirectory) {
|
|
|
357
388
|
return isAbsolute(source) ? source : resolve(baseDirectory, source);
|
|
358
389
|
}
|
|
359
390
|
|
|
391
|
+
// src/extension-status-metadata.ts
|
|
392
|
+
var UPDATE_CHANNEL = "extension-status:v1:update";
|
|
393
|
+
var REQUEST_CHANNEL = "extension-status:v1:request";
|
|
394
|
+
var ACTIVITIES = /* @__PURE__ */ new Set([
|
|
395
|
+
"idle",
|
|
396
|
+
"checking",
|
|
397
|
+
"working",
|
|
398
|
+
"pushing",
|
|
399
|
+
"pulling",
|
|
400
|
+
"resolving",
|
|
401
|
+
"retry",
|
|
402
|
+
"blocked",
|
|
403
|
+
"local-change",
|
|
404
|
+
"remote-change",
|
|
405
|
+
"review"
|
|
406
|
+
]);
|
|
407
|
+
function parseMetadata(data) {
|
|
408
|
+
if (Array.isArray(data)) return { withdrawal: false };
|
|
409
|
+
const { version, value, activity, reason } = data;
|
|
410
|
+
const hasValue = Object.hasOwn(data, "value");
|
|
411
|
+
const hasActivity = "activity" in data;
|
|
412
|
+
const hasReason = "reason" in data;
|
|
413
|
+
const withdrawal = version === 1 && hasValue && value === void 0 && !hasActivity && !hasReason;
|
|
414
|
+
if (version !== 1 || !hasValue || typeof value !== "string" || value.length === 0 || !ACTIVITIES.has(activity))
|
|
415
|
+
return { withdrawal };
|
|
416
|
+
if (activity === "retry" || activity === "blocked") {
|
|
417
|
+
if (typeof reason !== "string" || /^[a-z][a-z0-9-]{0,31}$/.exec(reason)?.[0] !== reason) return { withdrawal };
|
|
418
|
+
return { presentation: Object.freeze({ value, activity, reason }), withdrawal };
|
|
419
|
+
}
|
|
420
|
+
if (hasReason) return { withdrawal };
|
|
421
|
+
return { presentation: Object.freeze({ value, activity }), withdrawal };
|
|
422
|
+
}
|
|
423
|
+
function subscribeExtensionStatusMetadata(events, sessionManager, getStatuses, requestRender) {
|
|
424
|
+
const records = /* @__PURE__ */ new Map();
|
|
425
|
+
const liveKeys = /* @__PURE__ */ new Set();
|
|
426
|
+
const requestId = {};
|
|
427
|
+
let observedStatuses = /* @__PURE__ */ new Map();
|
|
428
|
+
try {
|
|
429
|
+
observedStatuses = new Map(getStatuses());
|
|
430
|
+
} catch {
|
|
431
|
+
}
|
|
432
|
+
let disposed = false;
|
|
433
|
+
let revision = 0;
|
|
434
|
+
const keyRevisions = /* @__PURE__ */ new Map();
|
|
435
|
+
const isCurrent = (key, owner) => !disposed && keyRevisions.get(key) === owner;
|
|
436
|
+
let unsubscribe;
|
|
437
|
+
const reconcile = (statuses, ownerKey) => {
|
|
438
|
+
if (disposed) return;
|
|
439
|
+
const before = revision;
|
|
440
|
+
const snapshot = new Map(statuses);
|
|
441
|
+
if (disposed || revision !== before) return;
|
|
442
|
+
for (const [key, value] of observedStatuses) {
|
|
443
|
+
if (snapshot.get(key) === value) continue;
|
|
444
|
+
records.delete(key);
|
|
445
|
+
liveKeys.add(key);
|
|
446
|
+
if (key !== ownerKey) keyRevisions.set(key, ++revision);
|
|
447
|
+
}
|
|
448
|
+
for (const key of snapshot.keys()) {
|
|
449
|
+
if (observedStatuses.has(key)) continue;
|
|
450
|
+
liveKeys.add(key);
|
|
451
|
+
if (key !== ownerKey) keyRevisions.set(key, ++revision);
|
|
452
|
+
}
|
|
453
|
+
observedStatuses = snapshot;
|
|
454
|
+
};
|
|
455
|
+
const metadata = {
|
|
456
|
+
get(key, value) {
|
|
457
|
+
const presentation = records.get(key);
|
|
458
|
+
return presentation?.value === value ? presentation : void 0;
|
|
459
|
+
},
|
|
460
|
+
reconcile(statuses) {
|
|
461
|
+
reconcile(statuses);
|
|
462
|
+
},
|
|
463
|
+
dispose() {
|
|
464
|
+
if (disposed) return;
|
|
465
|
+
disposed = true;
|
|
466
|
+
revision++;
|
|
467
|
+
keyRevisions.clear();
|
|
468
|
+
records.clear();
|
|
469
|
+
liveKeys.clear();
|
|
470
|
+
observedStatuses.clear();
|
|
471
|
+
try {
|
|
472
|
+
unsubscribe?.();
|
|
473
|
+
} catch {
|
|
474
|
+
}
|
|
475
|
+
unsubscribe = void 0;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const receive = (payload) => {
|
|
479
|
+
if (disposed || !payload || typeof payload !== "object") return;
|
|
480
|
+
const entered = revision;
|
|
481
|
+
let key;
|
|
482
|
+
let snapshot;
|
|
483
|
+
const data = payload;
|
|
484
|
+
try {
|
|
485
|
+
const rawKey = data.key;
|
|
486
|
+
const manager = data.sessionManager;
|
|
487
|
+
snapshot = "requestId" in data;
|
|
488
|
+
const replyId = snapshot ? data.requestId : void 0;
|
|
489
|
+
if (disposed || manager !== sessionManager || typeof rawKey !== "string" || !rawKey) return;
|
|
490
|
+
key = rawKey;
|
|
491
|
+
if ((keyRevisions.get(key) ?? 0) > entered) return;
|
|
492
|
+
if (snapshot && (replyId !== requestId || liveKeys.has(key))) return;
|
|
493
|
+
} catch {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const owner = ++revision;
|
|
497
|
+
keyRevisions.set(key, owner);
|
|
498
|
+
if (!snapshot) liveKeys.add(key);
|
|
499
|
+
const previous = records.get(key);
|
|
500
|
+
try {
|
|
501
|
+
const { presentation, withdrawal } = parseMetadata(data);
|
|
502
|
+
if (!isCurrent(key, owner)) return;
|
|
503
|
+
const reading = revision;
|
|
504
|
+
const statuses = new Map(getStatuses());
|
|
505
|
+
if (!isCurrent(key, owner)) return;
|
|
506
|
+
if (revision !== reading) {
|
|
507
|
+
records.delete(key);
|
|
508
|
+
} else {
|
|
509
|
+
reconcile(statuses, key);
|
|
510
|
+
if (!isCurrent(key, owner) || snapshot && liveKeys.has(key)) return;
|
|
511
|
+
if (presentation && statuses.get(key) === presentation.value) {
|
|
512
|
+
records.set(key, presentation);
|
|
513
|
+
} else if (!snapshot || withdrawal) {
|
|
514
|
+
records.delete(key);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
if (!isCurrent(key, owner)) return;
|
|
519
|
+
if (!snapshot) records.delete(key);
|
|
520
|
+
}
|
|
521
|
+
if (isCurrent(key, owner) && previous !== records.get(key)) {
|
|
522
|
+
try {
|
|
523
|
+
requestRender();
|
|
524
|
+
} catch {
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
try {
|
|
529
|
+
unsubscribe = events.on(UPDATE_CHANNEL, receive);
|
|
530
|
+
} catch {
|
|
531
|
+
return metadata;
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
events.emit(REQUEST_CHANNEL, { version: 1, sessionManager, requestId });
|
|
535
|
+
} catch {
|
|
536
|
+
}
|
|
537
|
+
return metadata;
|
|
538
|
+
}
|
|
539
|
+
|
|
360
540
|
// src/git-status.ts
|
|
361
541
|
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
362
542
|
var GIT_STATUS_TIMEOUT_MS = 3e3;
|
|
543
|
+
var GIT_STATUS_MAX_BUFFER = 1024 * 1024;
|
|
544
|
+
var GIT_ROOT_MAX_BUFFER = 16 * 1024;
|
|
363
545
|
async function readGitStatus(pi, cwd, signal) {
|
|
364
546
|
const [statusResult, rootResult] = await Promise.all([
|
|
365
547
|
pi.exec("git", ["--no-optional-locks", "status", "--porcelain=v1", "--branch", "--untracked-files=normal"], {
|
|
366
548
|
cwd,
|
|
367
549
|
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
550
|
+
maxBuffer: GIT_STATUS_MAX_BUFFER,
|
|
368
551
|
signal
|
|
369
552
|
}),
|
|
370
553
|
pi.exec("git", ["rev-parse", "--path-format=absolute", "--show-toplevel"], {
|
|
371
554
|
cwd,
|
|
372
555
|
timeout: GIT_STATUS_TIMEOUT_MS,
|
|
556
|
+
maxBuffer: GIT_ROOT_MAX_BUFFER,
|
|
373
557
|
signal
|
|
374
558
|
}).catch(() => void 0)
|
|
375
559
|
]);
|
|
376
|
-
if (statusResult.code !== 0 || statusResult.killed) return void 0;
|
|
560
|
+
if (statusResult.truncated || statusResult.code !== 0 || statusResult.killed) return void 0;
|
|
377
561
|
const summary = parseGitStatusPorcelain(statusResult.stdout);
|
|
378
|
-
if (rootResult && rootResult.code === 0 && !rootResult.killed) {
|
|
562
|
+
if (rootResult && !rootResult.truncated && rootResult.code === 0 && !rootResult.killed) {
|
|
379
563
|
const root = parseGitRoot(rootResult.stdout);
|
|
380
564
|
if (root) summary.root = root;
|
|
381
565
|
}
|
|
@@ -450,21 +634,21 @@ function formatCount(value) {
|
|
|
450
634
|
}
|
|
451
635
|
|
|
452
636
|
// src/render.ts
|
|
453
|
-
import { sliceByColumn, visibleWidth as
|
|
454
|
-
import { sanitizeTerminalText as
|
|
637
|
+
import { sliceByColumn, visibleWidth as visibleWidth3 } from "@earendil-works/pi-tui";
|
|
638
|
+
import { sanitizeTerminalText as sanitizeTerminalText3 } from "@narumitw/pi-tui-kit/terminal-text";
|
|
455
639
|
|
|
456
640
|
// src/directory.ts
|
|
457
641
|
import { basename, isAbsolute as isAbsolute3, relative, sep } from "node:path";
|
|
458
|
-
import { sanitizeTerminalText } from "@narumitw/pi-tui-kit/terminal-text";
|
|
642
|
+
import { sanitizeTerminalText as sanitizeTerminalText2 } from "@narumitw/pi-tui-kit/terminal-text";
|
|
459
643
|
var DEFAULT_TRUNCATION_LENGTH = 3;
|
|
460
644
|
var DEFAULT_HOME_SYMBOL = "~";
|
|
461
645
|
function formatDirectoryPath(cwd, homeDir, gitRoot) {
|
|
462
646
|
const contracted = gitRoot && (!homeDir || !samePath(gitRoot, homeDir)) ? contractRepositoryPath(cwd, gitRoot) ?? contractPath(cwd, homeDir, DEFAULT_HOME_SYMBOL) : contractPath(cwd, homeDir, DEFAULT_HOME_SYMBOL);
|
|
463
|
-
const safeContracted =
|
|
647
|
+
const safeContracted = sanitizeTerminalText2(contracted);
|
|
464
648
|
const components = safeContracted.split("/").filter((component) => component.length > 0);
|
|
465
649
|
const displayed = components.length > DEFAULT_TRUNCATION_LENGTH ? components.slice(-DEFAULT_TRUNCATION_LENGTH).join("/") : safeContracted;
|
|
466
650
|
const native = sep === "/" ? displayed : displayed.replaceAll("/", sep);
|
|
467
|
-
return
|
|
651
|
+
return sanitizeTerminalText2(native || basename(cwd) || cwd);
|
|
468
652
|
}
|
|
469
653
|
function toSlashPath(value) {
|
|
470
654
|
return sep === "\\" ? value.replaceAll("\\", "/") : value;
|
|
@@ -491,44 +675,12 @@ function samePath(left, right) {
|
|
|
491
675
|
return relative(left, right) === "";
|
|
492
676
|
}
|
|
493
677
|
|
|
494
|
-
// src/usage.ts
|
|
495
|
-
function summarizeFooterUsage(entries) {
|
|
496
|
-
const totals = {
|
|
497
|
-
input: 0,
|
|
498
|
-
output: 0,
|
|
499
|
-
cacheRead: 0,
|
|
500
|
-
cacheWrite: 0,
|
|
501
|
-
cost: 0
|
|
502
|
-
};
|
|
503
|
-
for (const entry of entries) {
|
|
504
|
-
let usage;
|
|
505
|
-
if (entry.type === "message" && entry.message.role === "assistant") {
|
|
506
|
-
usage = entry.message.usage;
|
|
507
|
-
const input = usage.input ?? 0;
|
|
508
|
-
const cacheRead = usage.cacheRead ?? 0;
|
|
509
|
-
const cacheWrite = usage.cacheWrite ?? 0;
|
|
510
|
-
const promptTokens = input + cacheRead + cacheWrite;
|
|
511
|
-
totals.latestCacheHitRate = promptTokens > 0 ? cacheRead / promptTokens * 100 : void 0;
|
|
512
|
-
} else if (entry.type === "message" && entry.message.role === "toolResult") {
|
|
513
|
-
usage = entry.message.usage;
|
|
514
|
-
} else if (entry.type === "compaction" || entry.type === "branch_summary" || entry.type === "usage") {
|
|
515
|
-
usage = entry.usage;
|
|
516
|
-
}
|
|
517
|
-
if (!usage) continue;
|
|
518
|
-
totals.input += usage.input ?? 0;
|
|
519
|
-
totals.output += usage.output ?? 0;
|
|
520
|
-
totals.cacheRead += usage.cacheRead ?? 0;
|
|
521
|
-
totals.cacheWrite += usage.cacheWrite ?? 0;
|
|
522
|
-
totals.cost += usage.cost?.total ?? 0;
|
|
523
|
-
}
|
|
524
|
-
return totals;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
678
|
// src/render.ts
|
|
528
679
|
var GITHUB_PR_KEY = "github-pr";
|
|
529
680
|
var GITHUB_PR_STATUS_KEYS = /* @__PURE__ */ new Set([GITHUB_PR_KEY]);
|
|
530
681
|
function renderStatusline(width, ctx, footerData, _theme, config, runtime, trueColor = true) {
|
|
531
682
|
if (width <= 0) return "";
|
|
683
|
+
const inlineStatuses = legacyInlineStatuses(footerData.getExtensionStatuses(), runtime);
|
|
532
684
|
const usageSummary = summarizeFooterUsage(ctx.sessionManager.getEntries());
|
|
533
685
|
const rows = [
|
|
534
686
|
{ configuredSegments: 0, segments: [] }
|
|
@@ -541,7 +693,7 @@ function renderStatusline(width, ctx, footerData, _theme, config, runtime, trueC
|
|
|
541
693
|
const row = rows.at(-1);
|
|
542
694
|
if (!row) continue;
|
|
543
695
|
row.configuredSegments += 1;
|
|
544
|
-
const rendered = buildSegment(name, ctx, footerData, config, runtime, usageSummary);
|
|
696
|
+
const rendered = buildSegment(name, ctx, footerData, config, runtime, usageSummary, inlineStatuses);
|
|
545
697
|
if (rendered && rendered.text.length > 0) row.segments.push(rendered);
|
|
546
698
|
}
|
|
547
699
|
const segments = [];
|
|
@@ -554,7 +706,7 @@ function renderStatusline(width, ctx, footerData, _theme, config, runtime, trueC
|
|
|
554
706
|
}
|
|
555
707
|
function renderExtensionStatusline(width, footerData, theme, config, runtime, mainLine, trueColor = true) {
|
|
556
708
|
const statuses = footerData.getExtensionStatuses();
|
|
557
|
-
const prContext = prContextFromStatuses(statuses);
|
|
709
|
+
const prContext = prContextFromStatuses(legacyInlineStatuses(statuses, runtime));
|
|
558
710
|
const rendersPrInline = prContext !== void 0 && mainLine.includes(prContext);
|
|
559
711
|
const status = formatExtensionStatuses(
|
|
560
712
|
statuses,
|
|
@@ -566,7 +718,13 @@ function renderExtensionStatusline(width, footerData, theme, config, runtime, ma
|
|
|
566
718
|
);
|
|
567
719
|
return wrapExtensionStatusline(status, width);
|
|
568
720
|
}
|
|
569
|
-
function
|
|
721
|
+
function legacyInlineStatuses(statuses, runtime) {
|
|
722
|
+
const metadata = runtime.extensionStatusMetadata;
|
|
723
|
+
if (!metadata) return statuses;
|
|
724
|
+
metadata.reconcile(statuses);
|
|
725
|
+
return new Map([...statuses].filter(([key, value]) => !metadata.get(key, value)));
|
|
726
|
+
}
|
|
727
|
+
function buildSegment(name, ctx, footerData, config, runtime, usageSummary, inlineStatuses) {
|
|
570
728
|
switch (name) {
|
|
571
729
|
case "brand":
|
|
572
730
|
return segment(name, "\u03C0", config, "accent", "header", true);
|
|
@@ -586,7 +744,7 @@ function buildSegment(name, ctx, footerData, config, runtime, usageSummary) {
|
|
|
586
744
|
return segment(name, runtime.thinkingLevel, config, thinkingColor(runtime.thinkingLevel), "header");
|
|
587
745
|
case "branch": {
|
|
588
746
|
const branch = footerData.getGitBranch();
|
|
589
|
-
const pr = branch ? prContextFromStatuses(
|
|
747
|
+
const pr = branch ? prContextFromStatuses(inlineStatuses) : void 0;
|
|
590
748
|
return segment(name, formatGitBranchValue(branch, runtime.gitStatus, pr), config, "accent", "git");
|
|
591
749
|
}
|
|
592
750
|
case "cwd":
|
|
@@ -691,9 +849,9 @@ function boundUIPromptTitleLength(title) {
|
|
|
691
849
|
return end < title.length ? `${title.slice(0, ellipsisEnd)}\u2026` : title;
|
|
692
850
|
}
|
|
693
851
|
function formatUIPromptTitle(title) {
|
|
694
|
-
const safeTitle = title ?
|
|
852
|
+
const safeTitle = title ? sanitizeTerminalText3(title).trim() : "";
|
|
695
853
|
const boundedTitle = boundUIPromptTitleLength(safeTitle);
|
|
696
|
-
if (
|
|
854
|
+
if (visibleWidth3(boundedTitle) <= MAX_UI_PROMPT_TITLE_WIDTH) return boundedTitle;
|
|
697
855
|
return `${sliceByColumn(boundedTitle, 0, MAX_UI_PROMPT_TITLE_WIDTH - 1, true)}\u2026`;
|
|
698
856
|
}
|
|
699
857
|
function formatToolActivity(runtime) {
|
|
@@ -762,11 +920,11 @@ function formatTime() {
|
|
|
762
920
|
}
|
|
763
921
|
var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
764
922
|
function truncateModel(model, length, symbol, direction) {
|
|
765
|
-
const safeModel =
|
|
923
|
+
const safeModel = sanitizeTerminalText3(model);
|
|
766
924
|
if (length === 0) return safeModel;
|
|
767
925
|
const graphemes = [...graphemeSegmenter.segment(safeModel)].map(({ segment: segment2 }) => segment2);
|
|
768
926
|
if (graphemes.length <= length) return safeModel;
|
|
769
|
-
const safeSymbol =
|
|
927
|
+
const safeSymbol = sanitizeTerminalText3(symbol);
|
|
770
928
|
switch (direction) {
|
|
771
929
|
case "start":
|
|
772
930
|
return `${safeSymbol}${graphemes.slice(-length).join("")}`;
|
|
@@ -792,7 +950,7 @@ var EMPTY_EXTENSION_STATUS_ICON_ALIASES2 = /* @__PURE__ */ new Map();
|
|
|
792
950
|
var statuslineCommandsPromise;
|
|
793
951
|
function loadStatuslineCommands() {
|
|
794
952
|
if (!statuslineCommandsPromise) {
|
|
795
|
-
statuslineCommandsPromise = import("./chunks/commands-
|
|
953
|
+
statuslineCommandsPromise = import("./chunks/commands-WV3JHCQ6.ts").catch((error) => {
|
|
796
954
|
statuslineCommandsPromise = void 0;
|
|
797
955
|
throw error;
|
|
798
956
|
});
|
|
@@ -803,6 +961,7 @@ function statusline(pi) {
|
|
|
803
961
|
let loaded;
|
|
804
962
|
let previewPalettePreset;
|
|
805
963
|
let activeSessionManager;
|
|
964
|
+
let disposeFooter;
|
|
806
965
|
const runtime = {
|
|
807
966
|
turnCount: 0,
|
|
808
967
|
activeTools: /* @__PURE__ */ new Map(),
|
|
@@ -879,6 +1038,7 @@ function statusline(pi) {
|
|
|
879
1038
|
scheduleGitStatusRefresh(activeGitStatusTarget.cwd, activeGitStatusTarget.generation);
|
|
880
1039
|
};
|
|
881
1040
|
const installFooter = (ctx) => {
|
|
1041
|
+
disposeFooter?.();
|
|
882
1042
|
const generation = ++sessionGeneration;
|
|
883
1043
|
menuController.abort(new DOMException("Statusline session context replaced", "AbortError"));
|
|
884
1044
|
menuController = new AbortController();
|
|
@@ -892,46 +1052,92 @@ function statusline(pi) {
|
|
|
892
1052
|
runtime.gitStatus = void 0;
|
|
893
1053
|
runtime.duplicateExtensions = [];
|
|
894
1054
|
runtime.extensionStatusIconAliases = EMPTY_EXTENSION_STATUS_ICON_ALIASES2;
|
|
1055
|
+
runtime.extensionStatusMetadata = void 0;
|
|
1056
|
+
runtime.requestRender = void 0;
|
|
895
1057
|
ctx.ui.setStatus(STATUSLINE_KEY2, void 0);
|
|
896
1058
|
if (!activeGitStatusTarget || !loaded) return;
|
|
897
1059
|
const installedPackages = readInstalledExtensionPackages(cwd);
|
|
898
1060
|
runtime.duplicateExtensions = findDuplicateExtensions(installedPackages);
|
|
899
1061
|
runtime.extensionStatusIconAliases = buildExtensionStatusIconAliases(installedPackages);
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
1062
|
+
const sessionManager = ctx.sessionManager;
|
|
1063
|
+
const createFooter = (tui, theme, footerData) => {
|
|
1064
|
+
if (generation !== sessionGeneration || sessionManager !== activeSessionManager) {
|
|
1065
|
+
return { render: () => [], invalidate() {
|
|
1066
|
+
} };
|
|
1067
|
+
}
|
|
1068
|
+
disposeFooter?.();
|
|
1069
|
+
activeGitStatusTarget = { cwd, generation };
|
|
1070
|
+
let disposed = false;
|
|
1071
|
+
let branchUnsubscribe;
|
|
1072
|
+
let clock;
|
|
1073
|
+
let metadata;
|
|
1074
|
+
const isCurrentFooter = () => !disposed && generation === sessionGeneration && sessionManager === activeSessionManager;
|
|
1075
|
+
const dispose = () => {
|
|
1076
|
+
if (disposed) return;
|
|
1077
|
+
disposed = true;
|
|
1078
|
+
try {
|
|
1079
|
+
metadata?.dispose();
|
|
1080
|
+
} finally {
|
|
1081
|
+
try {
|
|
1082
|
+
branchUnsubscribe?.();
|
|
1083
|
+
} finally {
|
|
1084
|
+
if (clock) clearInterval(clock);
|
|
1085
|
+
if (disposeFooter === dispose) {
|
|
1086
|
+
disposeFooter = void 0;
|
|
1087
|
+
runtime.extensionStatusMetadata = void 0;
|
|
1088
|
+
runtime.requestRender = void 0;
|
|
1089
|
+
}
|
|
1090
|
+
if (isActiveGitStatusTarget(cwd, generation)) {
|
|
1091
|
+
activeGitStatusTarget = void 0;
|
|
1092
|
+
abortGitStatusRefresh("Statusline footer disposed");
|
|
1093
|
+
clearGitStatusDebounce();
|
|
1094
|
+
pendingGitStatusRefresh = void 0;
|
|
1095
|
+
runtime.gitStatus = void 0;
|
|
1096
|
+
}
|
|
929
1097
|
}
|
|
930
|
-
}
|
|
1098
|
+
}
|
|
1099
|
+
};
|
|
1100
|
+
disposeFooter = dispose;
|
|
1101
|
+
runtime.requestRender = () => {
|
|
1102
|
+
if (isCurrentFooter()) tui.requestRender();
|
|
1103
|
+
};
|
|
1104
|
+
try {
|
|
1105
|
+
const refreshFooterGitStatus = () => refreshGitStatus(cwd, generation);
|
|
1106
|
+
branchUnsubscribe = footerData.onBranchChange(() => {
|
|
1107
|
+
if (!isCurrentFooter()) return;
|
|
1108
|
+
runtime.gitStatus = void 0;
|
|
1109
|
+
abortGitStatusRefresh("Statusline Git branch changed");
|
|
1110
|
+
clearGitStatusDebounce();
|
|
1111
|
+
refreshFooterGitStatus();
|
|
1112
|
+
refresh();
|
|
1113
|
+
});
|
|
1114
|
+
clock = setInterval(() => {
|
|
1115
|
+
if (!isCurrentFooter()) return;
|
|
1116
|
+
clearGitStatusDebounce();
|
|
1117
|
+
refreshFooterGitStatus();
|
|
1118
|
+
refresh();
|
|
1119
|
+
}, GIT_STATUS_REFRESH_INTERVAL_MS);
|
|
1120
|
+
clock.unref();
|
|
1121
|
+
metadata = subscribeExtensionStatusMetadata(
|
|
1122
|
+
pi.events,
|
|
1123
|
+
sessionManager,
|
|
1124
|
+
() => footerData.getExtensionStatuses(),
|
|
1125
|
+
() => {
|
|
1126
|
+
if (isCurrentFooter()) refresh();
|
|
1127
|
+
}
|
|
1128
|
+
);
|
|
1129
|
+
if (isCurrentFooter()) runtime.extensionStatusMetadata = metadata;
|
|
1130
|
+
else metadata.dispose();
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
dispose();
|
|
1133
|
+
throw error;
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
dispose,
|
|
931
1137
|
invalidate() {
|
|
932
1138
|
},
|
|
933
1139
|
render(width) {
|
|
934
|
-
if (!loaded) return [];
|
|
1140
|
+
if (!isCurrentFooter() || !loaded) return [];
|
|
935
1141
|
const config = previewPalettePreset ? { ...loaded.config, palettePreset: previewPalettePreset } : loaded.config;
|
|
936
1142
|
const trueColor = getCapabilities().trueColor;
|
|
937
1143
|
const mainLine = renderStatusline(width, ctx, footerData, theme, config, runtime, trueColor);
|
|
@@ -940,7 +1146,13 @@ function statusline(pi) {
|
|
|
940
1146
|
return lines;
|
|
941
1147
|
}
|
|
942
1148
|
};
|
|
943
|
-
}
|
|
1149
|
+
};
|
|
1150
|
+
try {
|
|
1151
|
+
ctx.ui.setFooter(createFooter);
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
disposeFooter?.();
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
944
1156
|
refreshGitStatus(cwd, generation);
|
|
945
1157
|
};
|
|
946
1158
|
const agentDir = getAgentDir2();
|
|
@@ -981,14 +1193,19 @@ function statusline(pi) {
|
|
|
981
1193
|
runtime.activeTools.clear();
|
|
982
1194
|
runtime.isStreaming = false;
|
|
983
1195
|
runtime.uiPrompt = void 0;
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
1196
|
+
try {
|
|
1197
|
+
loaded = loadStatuslineSettingsForAgent(agentDir);
|
|
1198
|
+
const settingsNotice = consumeStatuslineSettingsNotice();
|
|
1199
|
+
if (settingsNotice) ctx.ui.notify(settingsNotice, "warning");
|
|
1200
|
+
if (loaded.diagnostics.length > 0) {
|
|
1201
|
+
ctx.ui.notify(formatSettingsDiagnostics(loaded), "warning");
|
|
1202
|
+
}
|
|
1203
|
+
runtime.thinkingLevel = pi.getThinkingLevel();
|
|
1204
|
+
installFooter(ctx);
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
disposeFooter?.();
|
|
1207
|
+
throw error;
|
|
989
1208
|
}
|
|
990
|
-
runtime.thinkingLevel = pi.getThinkingLevel();
|
|
991
|
-
installFooter(ctx);
|
|
992
1209
|
});
|
|
993
1210
|
pi.on("session_tree", (_event, ctx) => {
|
|
994
1211
|
installFooter(ctx);
|
|
@@ -996,6 +1213,7 @@ function statusline(pi) {
|
|
|
996
1213
|
});
|
|
997
1214
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
998
1215
|
if (!ownsRuntime(ctx)) return;
|
|
1216
|
+
disposeFooter?.();
|
|
999
1217
|
sessionGeneration += 1;
|
|
1000
1218
|
menuController.abort(new DOMException("Statusline session shut down", "AbortError"));
|
|
1001
1219
|
activeSessionManager = void 0;
|
|
@@ -1010,7 +1228,7 @@ function statusline(pi) {
|
|
|
1010
1228
|
runtime.uiPrompt = void 0;
|
|
1011
1229
|
runtime.duplicateExtensions = [];
|
|
1012
1230
|
runtime.extensionStatusIconAliases = EMPTY_EXTENSION_STATUS_ICON_ALIASES2;
|
|
1013
|
-
ctx.ui.setFooter(void 0);
|
|
1231
|
+
if (ctx.mode === "tui") ctx.ui.setFooter(void 0);
|
|
1014
1232
|
ctx.ui.setStatus(STATUSLINE_KEY2, void 0);
|
|
1015
1233
|
runtime.requestRender = void 0;
|
|
1016
1234
|
});
|