@narumitw/pi-statusline 0.50.4 → 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 +6 -0
- package/README.md +6 -0
- package/dist/index.ts +304 -58
- package/dist/index.ts.map +4 -4
- package/docs/configuration.md +6 -0
- package/docs/extension-status-protocol.md +92 -0
- package/package.json +2 -2
- package/src/extension-status-metadata.ts +208 -0
- package/src/extension-status.ts +51 -5
- package/src/render.ts +15 -3
- package/src/statusline.ts +102 -41
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @narumitw/pi-statusline
|
|
2
2
|
|
|
3
|
+
## 0.51.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 379ebee: Add optional, extension-neutral status metadata v1 and a quiet footer presentation. Hide normal standby, shorten activity markers, and retain safe retry and blocked reasons while preserving configured icons and readable raw-status fallback. Keep sync authorization, scheduling, request limits, settings, notifications, and recovery behavior unchanged.
|
|
8
|
+
|
|
3
9
|
## 0.50.4
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -129,6 +129,12 @@ If the last remaining segment is itself wider than the row, that row renders emp
|
|
|
129
129
|
- Context color changes to warning at 70% and error at 90%.
|
|
130
130
|
- Git state is cached outside footer rendering and stale session results are ignored.
|
|
131
131
|
|
|
132
|
+
### Compact extension activity
|
|
133
|
+
|
|
134
|
+
Extensions can optionally supply [semantic status metadata v1](./docs/extension-status-protocol.md). Normal standby then disappears; checking uses `…`, work uses `↻`, and transfer directions use `↑` / `↓`. Pending-change arrows remain `⇡` / `⇣`; `⇕` means review is needed. Model resolution uses `AI`, while retries and blocked work retain short reasons such as `retry: limit` or `! auth`.
|
|
135
|
+
|
|
136
|
+
Configured icons keep their existing precedence, including an empty override. Other extensions, unsupported metadata, and other footers retain their readable raw-status behavior. This changes display only, not the publisher's work, authorization, or recovery policy.
|
|
137
|
+
|
|
132
138
|
### Usage and context
|
|
133
139
|
|
|
134
140
|
- `context` renders one-decimal current usage and the model window, such as `2.4%/272k`.
|
package/dist/index.ts
CHANGED
|
@@ -21,7 +21,8 @@ import { getCapabilities } from "@earendil-works/pi-tui";
|
|
|
21
21
|
import { existsSync, readFileSync } from "node:fs";
|
|
22
22
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
23
23
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
24
|
-
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";
|
|
25
26
|
|
|
26
27
|
// src/powerline.ts
|
|
27
28
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
@@ -171,10 +172,14 @@ function extensionStatusSeparator(config, theme, trueColor) {
|
|
|
171
172
|
return powerlineExtensionSeparator(theme, config.palettePreset, trueColor);
|
|
172
173
|
}
|
|
173
174
|
function formatExtensionStatuses(statuses, theme, config, runtime, hiddenKeys = /* @__PURE__ */ new Set(), trueColor = true) {
|
|
175
|
+
runtime.extensionStatusMetadata?.reconcile(statuses);
|
|
174
176
|
const separator = extensionStatusSeparator(config, theme, trueColor);
|
|
175
177
|
const visibleStatuses = [
|
|
176
178
|
...formatDuplicateExtensionStatus(runtime, theme),
|
|
177
|
-
...[...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)
|
|
178
183
|
].slice(0, 5);
|
|
179
184
|
return visibleStatuses.join(separator);
|
|
180
185
|
}
|
|
@@ -187,18 +192,41 @@ function formatExtensionStatus(key, value, theme, config, extensionStatusIconAli
|
|
|
187
192
|
const renderedText = theme.fg(textColor, text);
|
|
188
193
|
return icon ? `${theme.fg(color, icon)} ${renderedText}` : renderedText;
|
|
189
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
|
+
}
|
|
190
218
|
function extensionStatusIcon(key, leadingIcon, configuredIcons, extensionStatusIconAliases) {
|
|
191
219
|
if (Object.hasOwn(configuredIcons, key)) return configuredIcons[key];
|
|
192
220
|
const namespaceIcon = configuredNamespaceIcon(key, configuredIcons);
|
|
193
221
|
if (namespaceIcon !== void 0) return namespaceIcon;
|
|
194
|
-
const compatibleKey = COMPATIBLE_STATUS_ICON_KEYS[key];
|
|
222
|
+
const compatibleKey = Object.hasOwn(COMPATIBLE_STATUS_ICON_KEYS, key) ? COMPATIBLE_STATUS_ICON_KEYS[key] : void 0;
|
|
195
223
|
if (compatibleKey && Object.hasOwn(configuredIcons, compatibleKey)) {
|
|
196
224
|
return configuredIcons[compatibleKey];
|
|
197
225
|
}
|
|
198
226
|
for (const alias of extensionStatusAliasesForKey(key, extensionStatusIconAliases)) {
|
|
199
227
|
if (Object.hasOwn(configuredIcons, alias)) return configuredIcons[alias];
|
|
200
228
|
}
|
|
201
|
-
return leadingIcon ?? DEFAULT_EXTENSION_STATUS_ICONS[key]
|
|
229
|
+
return leadingIcon ?? (Object.hasOwn(DEFAULT_EXTENSION_STATUS_ICONS, key) ? DEFAULT_EXTENSION_STATUS_ICONS[key] : "\u{1F50C}");
|
|
202
230
|
}
|
|
203
231
|
function configuredNamespaceIcon(key, configuredIcons) {
|
|
204
232
|
let match;
|
|
@@ -221,7 +249,9 @@ function statusKeyMatchesStatusBase(key, statusBase) {
|
|
|
221
249
|
}
|
|
222
250
|
function wrapExtensionStatusline(status, width) {
|
|
223
251
|
if (!status || width <= 0) return [];
|
|
224
|
-
return wrapTextWithAnsi(status, width)
|
|
252
|
+
return wrapTextWithAnsi(status, width).map(
|
|
253
|
+
(line) => visibleWidth2(line) > width ? truncateToWidth(line, width, "") : line
|
|
254
|
+
);
|
|
225
255
|
}
|
|
226
256
|
function formatDuplicateExtensionStatus(runtime, theme) {
|
|
227
257
|
if (runtime.duplicateExtensions.length === 0) return [];
|
|
@@ -358,6 +388,155 @@ function resolveSourcePath(source, baseDirectory) {
|
|
|
358
388
|
return isAbsolute(source) ? source : resolve(baseDirectory, source);
|
|
359
389
|
}
|
|
360
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
|
+
|
|
361
540
|
// src/git-status.ts
|
|
362
541
|
import { isAbsolute as isAbsolute2 } from "node:path";
|
|
363
542
|
var GIT_STATUS_TIMEOUT_MS = 3e3;
|
|
@@ -455,21 +634,21 @@ function formatCount(value) {
|
|
|
455
634
|
}
|
|
456
635
|
|
|
457
636
|
// src/render.ts
|
|
458
|
-
import { sliceByColumn, visibleWidth as
|
|
459
|
-
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";
|
|
460
639
|
|
|
461
640
|
// src/directory.ts
|
|
462
641
|
import { basename, isAbsolute as isAbsolute3, relative, sep } from "node:path";
|
|
463
|
-
import { sanitizeTerminalText } from "@narumitw/pi-tui-kit/terminal-text";
|
|
642
|
+
import { sanitizeTerminalText as sanitizeTerminalText2 } from "@narumitw/pi-tui-kit/terminal-text";
|
|
464
643
|
var DEFAULT_TRUNCATION_LENGTH = 3;
|
|
465
644
|
var DEFAULT_HOME_SYMBOL = "~";
|
|
466
645
|
function formatDirectoryPath(cwd, homeDir, gitRoot) {
|
|
467
646
|
const contracted = gitRoot && (!homeDir || !samePath(gitRoot, homeDir)) ? contractRepositoryPath(cwd, gitRoot) ?? contractPath(cwd, homeDir, DEFAULT_HOME_SYMBOL) : contractPath(cwd, homeDir, DEFAULT_HOME_SYMBOL);
|
|
468
|
-
const safeContracted =
|
|
647
|
+
const safeContracted = sanitizeTerminalText2(contracted);
|
|
469
648
|
const components = safeContracted.split("/").filter((component) => component.length > 0);
|
|
470
649
|
const displayed = components.length > DEFAULT_TRUNCATION_LENGTH ? components.slice(-DEFAULT_TRUNCATION_LENGTH).join("/") : safeContracted;
|
|
471
650
|
const native = sep === "/" ? displayed : displayed.replaceAll("/", sep);
|
|
472
|
-
return
|
|
651
|
+
return sanitizeTerminalText2(native || basename(cwd) || cwd);
|
|
473
652
|
}
|
|
474
653
|
function toSlashPath(value) {
|
|
475
654
|
return sep === "\\" ? value.replaceAll("\\", "/") : value;
|
|
@@ -501,6 +680,7 @@ var GITHUB_PR_KEY = "github-pr";
|
|
|
501
680
|
var GITHUB_PR_STATUS_KEYS = /* @__PURE__ */ new Set([GITHUB_PR_KEY]);
|
|
502
681
|
function renderStatusline(width, ctx, footerData, _theme, config, runtime, trueColor = true) {
|
|
503
682
|
if (width <= 0) return "";
|
|
683
|
+
const inlineStatuses = legacyInlineStatuses(footerData.getExtensionStatuses(), runtime);
|
|
504
684
|
const usageSummary = summarizeFooterUsage(ctx.sessionManager.getEntries());
|
|
505
685
|
const rows = [
|
|
506
686
|
{ configuredSegments: 0, segments: [] }
|
|
@@ -513,7 +693,7 @@ function renderStatusline(width, ctx, footerData, _theme, config, runtime, trueC
|
|
|
513
693
|
const row = rows.at(-1);
|
|
514
694
|
if (!row) continue;
|
|
515
695
|
row.configuredSegments += 1;
|
|
516
|
-
const rendered = buildSegment(name, ctx, footerData, config, runtime, usageSummary);
|
|
696
|
+
const rendered = buildSegment(name, ctx, footerData, config, runtime, usageSummary, inlineStatuses);
|
|
517
697
|
if (rendered && rendered.text.length > 0) row.segments.push(rendered);
|
|
518
698
|
}
|
|
519
699
|
const segments = [];
|
|
@@ -526,7 +706,7 @@ function renderStatusline(width, ctx, footerData, _theme, config, runtime, trueC
|
|
|
526
706
|
}
|
|
527
707
|
function renderExtensionStatusline(width, footerData, theme, config, runtime, mainLine, trueColor = true) {
|
|
528
708
|
const statuses = footerData.getExtensionStatuses();
|
|
529
|
-
const prContext = prContextFromStatuses(statuses);
|
|
709
|
+
const prContext = prContextFromStatuses(legacyInlineStatuses(statuses, runtime));
|
|
530
710
|
const rendersPrInline = prContext !== void 0 && mainLine.includes(prContext);
|
|
531
711
|
const status = formatExtensionStatuses(
|
|
532
712
|
statuses,
|
|
@@ -538,7 +718,13 @@ function renderExtensionStatusline(width, footerData, theme, config, runtime, ma
|
|
|
538
718
|
);
|
|
539
719
|
return wrapExtensionStatusline(status, width);
|
|
540
720
|
}
|
|
541
|
-
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) {
|
|
542
728
|
switch (name) {
|
|
543
729
|
case "brand":
|
|
544
730
|
return segment(name, "\u03C0", config, "accent", "header", true);
|
|
@@ -558,7 +744,7 @@ function buildSegment(name, ctx, footerData, config, runtime, usageSummary) {
|
|
|
558
744
|
return segment(name, runtime.thinkingLevel, config, thinkingColor(runtime.thinkingLevel), "header");
|
|
559
745
|
case "branch": {
|
|
560
746
|
const branch = footerData.getGitBranch();
|
|
561
|
-
const pr = branch ? prContextFromStatuses(
|
|
747
|
+
const pr = branch ? prContextFromStatuses(inlineStatuses) : void 0;
|
|
562
748
|
return segment(name, formatGitBranchValue(branch, runtime.gitStatus, pr), config, "accent", "git");
|
|
563
749
|
}
|
|
564
750
|
case "cwd":
|
|
@@ -663,9 +849,9 @@ function boundUIPromptTitleLength(title) {
|
|
|
663
849
|
return end < title.length ? `${title.slice(0, ellipsisEnd)}\u2026` : title;
|
|
664
850
|
}
|
|
665
851
|
function formatUIPromptTitle(title) {
|
|
666
|
-
const safeTitle = title ?
|
|
852
|
+
const safeTitle = title ? sanitizeTerminalText3(title).trim() : "";
|
|
667
853
|
const boundedTitle = boundUIPromptTitleLength(safeTitle);
|
|
668
|
-
if (
|
|
854
|
+
if (visibleWidth3(boundedTitle) <= MAX_UI_PROMPT_TITLE_WIDTH) return boundedTitle;
|
|
669
855
|
return `${sliceByColumn(boundedTitle, 0, MAX_UI_PROMPT_TITLE_WIDTH - 1, true)}\u2026`;
|
|
670
856
|
}
|
|
671
857
|
function formatToolActivity(runtime) {
|
|
@@ -734,11 +920,11 @@ function formatTime() {
|
|
|
734
920
|
}
|
|
735
921
|
var graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
736
922
|
function truncateModel(model, length, symbol, direction) {
|
|
737
|
-
const safeModel =
|
|
923
|
+
const safeModel = sanitizeTerminalText3(model);
|
|
738
924
|
if (length === 0) return safeModel;
|
|
739
925
|
const graphemes = [...graphemeSegmenter.segment(safeModel)].map(({ segment: segment2 }) => segment2);
|
|
740
926
|
if (graphemes.length <= length) return safeModel;
|
|
741
|
-
const safeSymbol =
|
|
927
|
+
const safeSymbol = sanitizeTerminalText3(symbol);
|
|
742
928
|
switch (direction) {
|
|
743
929
|
case "start":
|
|
744
930
|
return `${safeSymbol}${graphemes.slice(-length).join("")}`;
|
|
@@ -775,6 +961,7 @@ function statusline(pi) {
|
|
|
775
961
|
let loaded;
|
|
776
962
|
let previewPalettePreset;
|
|
777
963
|
let activeSessionManager;
|
|
964
|
+
let disposeFooter;
|
|
778
965
|
const runtime = {
|
|
779
966
|
turnCount: 0,
|
|
780
967
|
activeTools: /* @__PURE__ */ new Map(),
|
|
@@ -851,6 +1038,7 @@ function statusline(pi) {
|
|
|
851
1038
|
scheduleGitStatusRefresh(activeGitStatusTarget.cwd, activeGitStatusTarget.generation);
|
|
852
1039
|
};
|
|
853
1040
|
const installFooter = (ctx) => {
|
|
1041
|
+
disposeFooter?.();
|
|
854
1042
|
const generation = ++sessionGeneration;
|
|
855
1043
|
menuController.abort(new DOMException("Statusline session context replaced", "AbortError"));
|
|
856
1044
|
menuController = new AbortController();
|
|
@@ -864,46 +1052,92 @@ function statusline(pi) {
|
|
|
864
1052
|
runtime.gitStatus = void 0;
|
|
865
1053
|
runtime.duplicateExtensions = [];
|
|
866
1054
|
runtime.extensionStatusIconAliases = EMPTY_EXTENSION_STATUS_ICON_ALIASES2;
|
|
1055
|
+
runtime.extensionStatusMetadata = void 0;
|
|
1056
|
+
runtime.requestRender = void 0;
|
|
867
1057
|
ctx.ui.setStatus(STATUSLINE_KEY2, void 0);
|
|
868
1058
|
if (!activeGitStatusTarget || !loaded) return;
|
|
869
1059
|
const installedPackages = readInstalledExtensionPackages(cwd);
|
|
870
1060
|
runtime.duplicateExtensions = findDuplicateExtensions(installedPackages);
|
|
871
1061
|
runtime.extensionStatusIconAliases = buildExtensionStatusIconAliases(installedPackages);
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
+
}
|
|
901
1097
|
}
|
|
902
|
-
}
|
|
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,
|
|
903
1137
|
invalidate() {
|
|
904
1138
|
},
|
|
905
1139
|
render(width) {
|
|
906
|
-
if (!loaded) return [];
|
|
1140
|
+
if (!isCurrentFooter() || !loaded) return [];
|
|
907
1141
|
const config = previewPalettePreset ? { ...loaded.config, palettePreset: previewPalettePreset } : loaded.config;
|
|
908
1142
|
const trueColor = getCapabilities().trueColor;
|
|
909
1143
|
const mainLine = renderStatusline(width, ctx, footerData, theme, config, runtime, trueColor);
|
|
@@ -912,7 +1146,13 @@ function statusline(pi) {
|
|
|
912
1146
|
return lines;
|
|
913
1147
|
}
|
|
914
1148
|
};
|
|
915
|
-
}
|
|
1149
|
+
};
|
|
1150
|
+
try {
|
|
1151
|
+
ctx.ui.setFooter(createFooter);
|
|
1152
|
+
} catch (error) {
|
|
1153
|
+
disposeFooter?.();
|
|
1154
|
+
throw error;
|
|
1155
|
+
}
|
|
916
1156
|
refreshGitStatus(cwd, generation);
|
|
917
1157
|
};
|
|
918
1158
|
const agentDir = getAgentDir2();
|
|
@@ -953,14 +1193,19 @@ function statusline(pi) {
|
|
|
953
1193
|
runtime.activeTools.clear();
|
|
954
1194
|
runtime.isStreaming = false;
|
|
955
1195
|
runtime.uiPrompt = void 0;
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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;
|
|
961
1208
|
}
|
|
962
|
-
runtime.thinkingLevel = pi.getThinkingLevel();
|
|
963
|
-
installFooter(ctx);
|
|
964
1209
|
});
|
|
965
1210
|
pi.on("session_tree", (_event, ctx) => {
|
|
966
1211
|
installFooter(ctx);
|
|
@@ -968,6 +1213,7 @@ function statusline(pi) {
|
|
|
968
1213
|
});
|
|
969
1214
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
970
1215
|
if (!ownsRuntime(ctx)) return;
|
|
1216
|
+
disposeFooter?.();
|
|
971
1217
|
sessionGeneration += 1;
|
|
972
1218
|
menuController.abort(new DOMException("Statusline session shut down", "AbortError"));
|
|
973
1219
|
activeSessionManager = void 0;
|
|
@@ -982,7 +1228,7 @@ function statusline(pi) {
|
|
|
982
1228
|
runtime.uiPrompt = void 0;
|
|
983
1229
|
runtime.duplicateExtensions = [];
|
|
984
1230
|
runtime.extensionStatusIconAliases = EMPTY_EXTENSION_STATUS_ICON_ALIASES2;
|
|
985
|
-
ctx.ui.setFooter(void 0);
|
|
1231
|
+
if (ctx.mode === "tui") ctx.ui.setFooter(void 0);
|
|
986
1232
|
ctx.ui.setStatus(STATUSLINE_KEY2, void 0);
|
|
987
1233
|
runtime.requestRender = void 0;
|
|
988
1234
|
});
|