@underactive/pi-topping-statusline 0.2.0 → 0.2.2
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 +62 -16
- package/index.ts +214 -117
- package/package.json +4 -5
- package/src/box.ts +33 -11
- package/src/compaction-embed.ts +62 -0
- package/src/context-graph.ts +5 -5
- package/src/context-thresholds.ts +7 -3
- package/src/context.ts +72 -13
- package/src/footer.ts +5 -2
- package/src/layout.ts +81 -30
- package/src/menu.ts +25 -36
- package/src/nvidia-green.ts +66 -0
- package/src/rainbow.ts +18 -4
- package/src/segments.ts +77 -5
- package/src/settings-menu.ts +102 -47
- package/src/settings.ts +30 -4
- package/src/tests/box.test.ts +6 -6
- package/src/tests/compaction-embed.test.ts +184 -0
- package/src/tests/context-estimate.test.ts +207 -0
- package/src/tests/context-thresholds.test.ts +6 -0
- package/src/tests/feeds.test.ts +2 -8
- package/src/tests/footer.test.ts +1 -0
- package/src/tests/menu.test.ts +31 -2
- package/src/tests/nvidia-green.test.ts +51 -0
- package/src/tests/token-rate.test.ts +2 -1
- package/src/tests/working-status-embed.test.ts +12 -0
- package/src/tests/working-status.test.ts +209 -0
- package/src/tests/working.test.ts +239 -23
- package/src/token-rate.ts +1 -1
- package/src/types.ts +37 -5
- package/src/working-status-embed.ts +15 -0
- package/src/working-status.ts +125 -0
package/src/menu.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* ║ ╭── π ⬢ model ───────────────────── session ──╮ ║
|
|
17
17
|
* ║ ║
|
|
18
18
|
* ╟─ Global ─────────────────────────────────────────╢
|
|
19
|
-
* ║
|
|
19
|
+
* ║ > [■] Transparent Segments ON ║
|
|
20
20
|
* ║ Separator ‹ powerline-thin › ║
|
|
21
21
|
* ║ ║
|
|
22
22
|
* ╟──────────────────────────────────────────────────╢
|
|
@@ -118,11 +118,6 @@ const PLAIN_ROW_PREFIX_LEN = 5;
|
|
|
118
118
|
const CHECKBOX_ROW_PREFIX_LEN = 8;
|
|
119
119
|
|
|
120
120
|
interface FlatItem {
|
|
121
|
-
id: string;
|
|
122
|
-
label: string;
|
|
123
|
-
cycleValues?: readonly string[];
|
|
124
|
-
cycleEnabledBy?: string;
|
|
125
|
-
cycleDisabledValue?: string;
|
|
126
121
|
item: MenuItem;
|
|
127
122
|
sectionIndex: number;
|
|
128
123
|
}
|
|
@@ -131,15 +126,7 @@ function flattenSections(sections: MenuSection[]): FlatItem[] {
|
|
|
131
126
|
const flat: FlatItem[] = [];
|
|
132
127
|
for (const [sectionIndex, section] of sections.entries()) {
|
|
133
128
|
for (const item of section.items) {
|
|
134
|
-
flat.push({
|
|
135
|
-
id: item.id,
|
|
136
|
-
label: item.label,
|
|
137
|
-
cycleValues: item.cycleValues,
|
|
138
|
-
cycleEnabledBy: item.cycleEnabledBy,
|
|
139
|
-
cycleDisabledValue: item.cycleDisabledValue,
|
|
140
|
-
item,
|
|
141
|
-
sectionIndex,
|
|
142
|
-
});
|
|
129
|
+
flat.push({ item, sectionIndex });
|
|
143
130
|
}
|
|
144
131
|
}
|
|
145
132
|
return flat;
|
|
@@ -260,26 +247,26 @@ export class MenuComponent implements Component {
|
|
|
260
247
|
[Key.space]: () => {
|
|
261
248
|
const item = this.flat[this.cursor]!;
|
|
262
249
|
if (item.item.action) {
|
|
263
|
-
this.runAction(item.id);
|
|
250
|
+
this.runAction(item.item.id);
|
|
264
251
|
return;
|
|
265
252
|
}
|
|
266
253
|
if (item.item.text) {
|
|
267
254
|
this.beginEditing(item);
|
|
268
255
|
return;
|
|
269
256
|
}
|
|
270
|
-
if (item.cycleValues && item.cycleEnabledBy) {
|
|
271
|
-
this.values[item.cycleEnabledBy] = !this.values[item.cycleEnabledBy] as boolean;
|
|
272
|
-
if (!this.values[item.cycleEnabledBy] && item.cycleDisabledValue !== undefined) {
|
|
273
|
-
this.values[item.id] = item.cycleDisabledValue;
|
|
257
|
+
if (item.item.cycleValues && item.item.cycleEnabledBy) {
|
|
258
|
+
this.values[item.item.cycleEnabledBy] = !this.values[item.item.cycleEnabledBy] as boolean;
|
|
259
|
+
if (!this.values[item.item.cycleEnabledBy] && item.item.cycleDisabledValue !== undefined) {
|
|
260
|
+
this.values[item.item.id] = item.item.cycleDisabledValue;
|
|
274
261
|
}
|
|
275
|
-
} else if (!item.cycleValues) this.values[item.id] = !this.values[item.id] as boolean;
|
|
262
|
+
} else if (!item.item.cycleValues) this.values[item.item.id] = !this.values[item.item.id] as boolean;
|
|
276
263
|
this.invalidate();
|
|
277
264
|
},
|
|
278
265
|
// Enter opens an editor or fires an action on those rows, so applying
|
|
279
266
|
// the whole menu stays available from any other row.
|
|
280
267
|
[Key.enter]: () => {
|
|
281
268
|
const item = this.flat[this.cursor]!;
|
|
282
|
-
if (item.item.action) this.runAction(item.id);
|
|
269
|
+
if (item.item.action) this.runAction(item.item.id);
|
|
283
270
|
else if (item.item.text) this.beginEditing(item);
|
|
284
271
|
else this.done({ applied: true, values: { ...this.values } });
|
|
285
272
|
},
|
|
@@ -297,7 +284,7 @@ export class MenuComponent implements Component {
|
|
|
297
284
|
}
|
|
298
285
|
|
|
299
286
|
private beginEditing(item: FlatItem): void {
|
|
300
|
-
this.editing = { id: item.id, buffer: String(this.values[item.id] ?? "") };
|
|
287
|
+
this.editing = { id: item.item.id, buffer: String(this.values[item.item.id] ?? "") };
|
|
301
288
|
this.invalidate();
|
|
302
289
|
}
|
|
303
290
|
|
|
@@ -373,10 +360,10 @@ export class MenuComponent implements Component {
|
|
|
373
360
|
|
|
374
361
|
private cycleCurrentValue(delta: number): void {
|
|
375
362
|
const item = this.flat[this.cursor]!;
|
|
376
|
-
if (!item.cycleValues?.length || (item.cycleEnabledBy && !this.values[item.cycleEnabledBy])) return;
|
|
377
|
-
const current = item.cycleValues.indexOf(this.values[item.id] as string);
|
|
378
|
-
const index = (current + delta + item.cycleValues.length) % item.cycleValues.length;
|
|
379
|
-
this.values[item.id] = item.cycleValues[index]!;
|
|
363
|
+
if (!item.item.cycleValues?.length || (item.item.cycleEnabledBy && !this.values[item.item.cycleEnabledBy])) return;
|
|
364
|
+
const current = item.item.cycleValues.indexOf(this.values[item.item.id] as string);
|
|
365
|
+
const index = (current + delta + item.item.cycleValues.length) % item.item.cycleValues.length;
|
|
366
|
+
this.values[item.item.id] = item.item.cycleValues[index]!;
|
|
380
367
|
this.invalidate();
|
|
381
368
|
}
|
|
382
369
|
|
|
@@ -386,7 +373,7 @@ export class MenuComponent implements Component {
|
|
|
386
373
|
const result = this.previewFn(
|
|
387
374
|
this.values,
|
|
388
375
|
this.previewOrigin !== undefined ? Date.now() - this.previewOrigin : 0,
|
|
389
|
-
this.flat[this.cursor]?.id,
|
|
376
|
+
this.flat[this.cursor]?.item.id,
|
|
390
377
|
innerWidth,
|
|
391
378
|
);
|
|
392
379
|
if (Array.isArray(result)) {
|
|
@@ -500,8 +487,8 @@ export class MenuComponent implements Component {
|
|
|
500
487
|
}
|
|
501
488
|
|
|
502
489
|
private availableRows(): number | undefined {
|
|
503
|
-
const rows =
|
|
504
|
-
return
|
|
490
|
+
const rows = this.tui?.terminal.rows;
|
|
491
|
+
return rows !== undefined && rows > 0 ? rows : undefined;
|
|
505
492
|
}
|
|
506
493
|
|
|
507
494
|
private buildLines(maxWidth: number, maxRows?: number): string[] {
|
|
@@ -597,15 +584,17 @@ export class MenuComponent implements Component {
|
|
|
597
584
|
private renderItemRow(item: MenuItem, selected: boolean, innerWidth: number): string {
|
|
598
585
|
const th = this.theme;
|
|
599
586
|
const value = this.values[item.id]!;
|
|
600
|
-
const marker = selected ? "
|
|
587
|
+
const marker = selected ? ">" : " ";
|
|
601
588
|
const markerColored = selected ? th.fg("accent", marker) : marker;
|
|
589
|
+
const rowBackground = (content: string) =>
|
|
590
|
+
selected ? th.bg("selectedBg", content.replaceAll("\x1b[0m", `\x1b[0m${th.getBgAnsi("selectedBg")}`)) : content;
|
|
602
591
|
|
|
603
592
|
if (item.action) {
|
|
604
593
|
const label = truncateToWidth(item.label, Math.max(0, innerWidth - 5));
|
|
605
594
|
const leftPlain = ` ${marker} ${label}`;
|
|
606
595
|
const pad = Math.max(0, innerWidth - visibleWidth(leftPlain));
|
|
607
596
|
const content = ` ${markerColored} ${th.fg(selected ? "accent" : "muted", label)}${" ".repeat(pad)}`;
|
|
608
|
-
return this.wrap("\u2551", content, "\u2551");
|
|
597
|
+
return this.wrap("\u2551", rowBackground(content), "\u2551");
|
|
609
598
|
}
|
|
610
599
|
|
|
611
600
|
if (item.text) {
|
|
@@ -626,7 +615,7 @@ export class MenuComponent implements Component {
|
|
|
626
615
|
const leftPlain = ` ${marker} ${label} ${fieldText}${caret}`;
|
|
627
616
|
const pad = Math.max(0, innerWidth - visibleWidth(leftPlain) - 1);
|
|
628
617
|
const content = ` ${markerColored} ${th.fg("dim", label)} ${fieldStyled}${editing ? th.fg("accent", caret) : ""}${" ".repeat(pad)} `;
|
|
629
|
-
return this.wrap("\u2551", content, "\u2551");
|
|
618
|
+
return this.wrap("\u2551", rowBackground(content), "\u2551");
|
|
630
619
|
}
|
|
631
620
|
|
|
632
621
|
if (item.cycleValues) {
|
|
@@ -638,7 +627,7 @@ export class MenuComponent implements Component {
|
|
|
638
627
|
const leftPlain = ` ${marker} ${label}`;
|
|
639
628
|
const gap = Math.max(1, innerWidth - visibleWidth(leftPlain) - visibleWidth(stateWord) - 2);
|
|
640
629
|
const content = ` ${markerColored} ${th.fg("text", label)}${" ".repeat(gap)}${th.fg("accent", stateWord)} `;
|
|
641
|
-
return this.wrap("\u2551", content, "\u2551");
|
|
630
|
+
return this.wrap("\u2551", rowBackground(content), "\u2551");
|
|
642
631
|
}
|
|
643
632
|
const enabled = this.values[item.cycleEnabledBy] as boolean;
|
|
644
633
|
const maxLabelLen = Math.max(0, innerWidth - CHECKBOX_ROW_PREFIX_LEN - visibleWidth(stateWord) - 1);
|
|
@@ -646,7 +635,7 @@ export class MenuComponent implements Component {
|
|
|
646
635
|
const leftPlain = ` ${marker} [${enabled ? "■" : " "}] ${label}`;
|
|
647
636
|
const gap = Math.max(1, innerWidth - visibleWidth(leftPlain) - visibleWidth(stateWord) - 2);
|
|
648
637
|
const content = ` ${markerColored} [${enabled ? th.fg("success", "■") : th.fg("muted", " ")}] ${th.fg("text", label)}${" ".repeat(gap)}${enabled ? th.fg("accent", stateWord) : th.fg("muted", stateWord)} `;
|
|
649
|
-
return this.wrap("\u2551", content, "\u2551");
|
|
638
|
+
return this.wrap("\u2551", rowBackground(content), "\u2551");
|
|
650
639
|
}
|
|
651
640
|
|
|
652
641
|
const enabled = value as boolean;
|
|
@@ -658,7 +647,7 @@ export class MenuComponent implements Component {
|
|
|
658
647
|
const leftPlain = ` ${marker} [${box}] ${label}`;
|
|
659
648
|
const gap = Math.max(1, innerWidth - visibleWidth(leftPlain) - visibleWidth(rightPlain));
|
|
660
649
|
const content = ` ${markerColored} [${enabled ? th.fg("success", box) : th.fg("muted", box)}] ${th.fg("text", label)}${" ".repeat(gap)}${enabled ? th.fg("success", stateWord) : th.fg("muted", stateWord)} `;
|
|
661
|
-
return this.wrap("\u2551", content, "\u2551");
|
|
650
|
+
return this.wrap("\u2551", rowBackground(content), "\u2551");
|
|
662
651
|
}
|
|
663
652
|
}
|
|
664
653
|
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/** Provider-specific NVIDIA green border colorizer for Switchyard. */
|
|
2
|
+
import { rgbToHex, wrapDeg, type BorderColorizer } from "./rainbow.js";
|
|
3
|
+
import { detectColorMode, hexToFgAnsi, parseHex, type ColorMode } from "./theme.js";
|
|
4
|
+
|
|
5
|
+
export const SWITCHYARD_PROVIDER = "switchyard";
|
|
6
|
+
export const NVIDIA_GREEN_HEX = "#84c51a";
|
|
7
|
+
export const NVIDIA_GREEN_DARK_HEX = "#0b3d20";
|
|
8
|
+
const GREEN_SHADE_COUNT = 256;
|
|
9
|
+
const GREEN_BIAS = 0.65;
|
|
10
|
+
|
|
11
|
+
export function isSwitchyardProvider(provider: string | undefined): boolean {
|
|
12
|
+
return provider?.trim().toLowerCase() === SWITCHYARD_PROVIDER;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ansiByShadeByMode = new Map<ColorMode, string[]>();
|
|
16
|
+
|
|
17
|
+
function buildAnsiByShade(mode: ColorMode): string[] {
|
|
18
|
+
const cached = ansiByShadeByMode.get(mode);
|
|
19
|
+
if (cached !== undefined) return cached;
|
|
20
|
+
|
|
21
|
+
const dark = parseHex(NVIDIA_GREEN_DARK_HEX);
|
|
22
|
+
const bright = parseHex(NVIDIA_GREEN_HEX);
|
|
23
|
+
if (dark === undefined || bright === undefined) {
|
|
24
|
+
throw new Error("Invalid NVIDIA green palette");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const ansiByShade: string[] = [];
|
|
28
|
+
for (let i = 0; i < GREEN_SHADE_COUNT; i++) {
|
|
29
|
+
const progress = i / (GREEN_SHADE_COUNT - 1);
|
|
30
|
+
const rgb: [number, number, number] = [
|
|
31
|
+
Math.round(dark[0] + (bright[0] - dark[0]) * progress),
|
|
32
|
+
Math.round(dark[1] + (bright[1] - dark[1]) * progress),
|
|
33
|
+
Math.round(dark[2] + (bright[2] - dark[2]) * progress),
|
|
34
|
+
];
|
|
35
|
+
ansiByShade.push(hexToFgAnsi(rgbToHex(rgb), mode));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
ansiByShadeByMode.set(mode, ansiByShade);
|
|
39
|
+
return ansiByShade;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class NvidiaGreenBorder implements BorderColorizer {
|
|
43
|
+
#phaseDeg: number;
|
|
44
|
+
readonly #ansiByShade: string[];
|
|
45
|
+
|
|
46
|
+
constructor(phaseDeg = 0) {
|
|
47
|
+
this.#phaseDeg = wrapDeg(phaseDeg);
|
|
48
|
+
this.#ansiByShade = buildAnsiByShade(detectColorMode());
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
step(deltaDeg: number): void {
|
|
52
|
+
this.#phaseDeg = wrapDeg(this.#phaseDeg + deltaDeg);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
colorChar(char: string, perimeterPos: number, perimeter: number): string {
|
|
56
|
+
return `${this.prefix(perimeterPos, perimeter)}${char}\x1b[39m`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
prefix(perimeterPos: number, perimeter: number): string {
|
|
60
|
+
const t = (perimeterPos / perimeter + this.#phaseDeg / 360) % 1;
|
|
61
|
+
const u = (1 - Math.cos(2 * Math.PI * t)) / 2;
|
|
62
|
+
const eased = u ** GREEN_BIAS;
|
|
63
|
+
const shadeIndex = Math.round((1 - eased) * (GREEN_SHADE_COUNT - 1));
|
|
64
|
+
return this.#ansiByShade[shadeIndex];
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/rainbow.ts
CHANGED
|
@@ -24,6 +24,9 @@ export const RAINBOW_DEG_PER_FRAME = (360 * RAINBOW_FRAME_MS) / RAINBOW_CYCLE_MS
|
|
|
24
24
|
const SATURATION = 1;
|
|
25
25
|
const LIGHTNESS = 0.62;
|
|
26
26
|
|
|
27
|
+
/** Normalize a degree value into the [0, 360) range. */
|
|
28
|
+
export const wrapDeg = (deg: number): number => ((deg % 360) + 360) % 360;
|
|
29
|
+
|
|
27
30
|
/**
|
|
28
31
|
* Number of border cells in the box (rows 0..bottomIdx, cols 0..width-1):
|
|
29
32
|
* two horizontal edges of `width` cells plus two vertical edges of
|
|
@@ -64,27 +67,38 @@ export function rgbToHex([r, g, b]: [number, number, number]): string {
|
|
|
64
67
|
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`;
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
export interface BorderColorizer {
|
|
71
|
+
colorChar(ch: string, perimeterPos: number, perimeter: number): string;
|
|
72
|
+
/** ANSI foreground-color escape for a perimeter position, with no glyph or reset. */
|
|
73
|
+
prefix(perimeterPos: number, perimeter: number): string;
|
|
74
|
+
step(deltaDeg: number): void;
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
/**
|
|
68
78
|
* Colors box glyphs with a hue that travels around the border perimeter as
|
|
69
79
|
* the phase advances — the spectrum appears to flow around the box.
|
|
70
80
|
*/
|
|
71
|
-
export class RainbowBorder {
|
|
81
|
+
export class RainbowBorder implements BorderColorizer {
|
|
72
82
|
#phaseDeg: number;
|
|
73
83
|
readonly #mode: ColorMode;
|
|
74
84
|
readonly #ansiByHue: string[] = [];
|
|
75
85
|
|
|
76
86
|
constructor(phaseDeg = 0) {
|
|
77
|
-
this.#phaseDeg = (
|
|
87
|
+
this.#phaseDeg = wrapDeg(phaseDeg);
|
|
78
88
|
this.#mode = detectColorMode();
|
|
79
89
|
}
|
|
80
90
|
|
|
81
91
|
/** Advance the whole spectrum by `deltaDeg` (mod 360). */
|
|
82
92
|
step(deltaDeg: number): void {
|
|
83
|
-
this.#phaseDeg = (
|
|
93
|
+
this.#phaseDeg = wrapDeg(this.#phaseDeg + deltaDeg);
|
|
84
94
|
}
|
|
85
95
|
|
|
86
96
|
/** Color one border glyph whose walk position is `perimeterPos` of `perimeter`. */
|
|
87
97
|
colorChar(char: string, perimeterPos: number, perimeter: number): string {
|
|
98
|
+
return `${this.prefix(perimeterPos, perimeter)}${char}\x1b[39m`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
prefix(perimeterPos: number, perimeter: number): string {
|
|
88
102
|
const hue = (this.#phaseDeg + (360 * perimeterPos) / perimeter) % 360;
|
|
89
103
|
const bucket = Math.round(hue) % 360;
|
|
90
104
|
let ansi = this.#ansiByHue[bucket];
|
|
@@ -93,6 +107,6 @@ export class RainbowBorder {
|
|
|
93
107
|
ansi = hexToFgAnsi(hex, this.#mode);
|
|
94
108
|
this.#ansiByHue[bucket] = ansi;
|
|
95
109
|
}
|
|
96
|
-
return
|
|
110
|
+
return ansi;
|
|
97
111
|
}
|
|
98
112
|
}
|
package/src/segments.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { MAJOR_COLOR_HEXES, hexToFgAnsi, theme } from "./theme.js";
|
|
|
14
14
|
import {
|
|
15
15
|
clampPathLength,
|
|
16
16
|
feedKey,
|
|
17
|
+
formatNumber,
|
|
17
18
|
getSessionAccentHex,
|
|
18
19
|
sanitizeLabel,
|
|
19
20
|
sanitizeStatusText,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
withIcon,
|
|
22
23
|
} from "./utils.js";
|
|
23
24
|
import type {
|
|
25
|
+
CompactionProgressView,
|
|
24
26
|
FeedFormat,
|
|
25
27
|
RenderedSegment,
|
|
26
28
|
SegmentContext,
|
|
@@ -75,6 +77,24 @@ function classifyProjectDir(pwd: string): { scratch: boolean; relative: string |
|
|
|
75
77
|
return { scratch: false, relative: null };
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
interface PathClassification {
|
|
81
|
+
scratch: boolean;
|
|
82
|
+
relative: string | null;
|
|
83
|
+
strippedDisplay: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let cachedCwd: string | undefined;
|
|
87
|
+
let cachedClassification: PathClassification | undefined;
|
|
88
|
+
|
|
89
|
+
/** cwd is effectively constant for a session, so cache the root scans instead of redoing them on every render (including repeated shrink passes). */
|
|
90
|
+
function classifyCwd(cwd: string): PathClassification {
|
|
91
|
+
if (cachedCwd === cwd && cachedClassification) return cachedClassification;
|
|
92
|
+
const { scratch, relative } = classifyProjectDir(cwd);
|
|
93
|
+
cachedClassification = { scratch, relative, strippedDisplay: stripDisplayRoot(cwd) };
|
|
94
|
+
cachedCwd = cwd;
|
|
95
|
+
return cachedClassification;
|
|
96
|
+
}
|
|
97
|
+
|
|
78
98
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
79
99
|
// Segment implementations
|
|
80
100
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -133,14 +153,14 @@ const pathSegment: StatusLineSegment = {
|
|
|
133
153
|
return { content: theme.fg("statusLinePath", content), visible: true };
|
|
134
154
|
}
|
|
135
155
|
|
|
136
|
-
const { scratch, relative } =
|
|
156
|
+
const { scratch, relative, strippedDisplay } = classifyCwd(ctx.cwd);
|
|
137
157
|
let pwd = ctx.cwd;
|
|
138
158
|
|
|
139
159
|
if (opts.stripWorkPrefix) {
|
|
140
160
|
if (scratch) {
|
|
141
161
|
if (relative) pwd = relative;
|
|
142
162
|
} else {
|
|
143
|
-
pwd =
|
|
163
|
+
pwd = strippedDisplay;
|
|
144
164
|
}
|
|
145
165
|
}
|
|
146
166
|
if (opts.abbreviate) {
|
|
@@ -296,9 +316,9 @@ const scrollHintSegment: StatusLineSegment = {
|
|
|
296
316
|
};
|
|
297
317
|
|
|
298
318
|
/**
|
|
299
|
-
* Pi's embedded
|
|
300
|
-
* host, so it is passed through verbatim. The layout
|
|
301
|
-
*
|
|
319
|
+
* Pi's embedded status spinner (spinner + message), already colored by the
|
|
320
|
+
* host, so it is passed through verbatim. The layout applies the caller's
|
|
321
|
+
* truncation style: a bare cut for working, ellipsis for message-style kinds.
|
|
302
322
|
*/
|
|
303
323
|
const workingSegment: StatusLineSegment = {
|
|
304
324
|
id: "working",
|
|
@@ -326,12 +346,62 @@ const contextGraphSegment: StatusLineSegment = {
|
|
|
326
346
|
labelColor: theme.getFgAnsi("dim"),
|
|
327
347
|
showBar,
|
|
328
348
|
showStats,
|
|
349
|
+
estimated: ctx.contextEstimated === true,
|
|
329
350
|
}),
|
|
330
351
|
visible: true,
|
|
331
352
|
};
|
|
332
353
|
},
|
|
333
354
|
};
|
|
334
355
|
|
|
356
|
+
const clampPercent = (percent: number): number => Math.round(Math.min(100, Math.max(0, percent)));
|
|
357
|
+
|
|
358
|
+
/** "57% summarized · 16.3s": how much of the context has been summarized so far, and the time taken. */
|
|
359
|
+
export function formatCompactionInfo(view: CompactionProgressView): string {
|
|
360
|
+
const elapsed = `${(Math.max(0, view.elapsedMs) / 1000).toFixed(1)}s`;
|
|
361
|
+
return `${clampPercent(view.percent)}% summarized${theme.sep.dot}${elapsed}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* "86K/131K (66%)": the tokens being compacted over the window, and as a share
|
|
366
|
+
* of it, in the context label's number style. The share is omitted when the
|
|
367
|
+
* window is unknown.
|
|
368
|
+
*/
|
|
369
|
+
export function formatCompactionTokens(view: CompactionProgressView): string {
|
|
370
|
+
const before = formatNumber(Math.max(0, view.tokensBefore));
|
|
371
|
+
if (view.contextWindow <= 0) return `${before}/?`;
|
|
372
|
+
const usage = clampPercent((view.tokensBefore / view.contextWindow) * 100);
|
|
373
|
+
return `${before}/${formatNumber(view.contextWindow)} (${usage}%)`;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** pi-topping-compact's figures, standing in for pi's stats while its progress is hosted. */
|
|
377
|
+
const compactionInfoSegment: StatusLineSegment = {
|
|
378
|
+
id: "compaction_info",
|
|
379
|
+
render(ctx) {
|
|
380
|
+
const view = ctx.compaction;
|
|
381
|
+
if (!view) return INVISIBLE;
|
|
382
|
+
return { content: theme.fg("dim", formatCompactionInfo(view)), visible: true };
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* pi-topping-compact's bar, standing in for the context graph while its progress
|
|
388
|
+
* is hosted, under the graph's own bar and stats toggles. The bar arrives
|
|
389
|
+
* pre-rendered with fg-only resets, so it is passed through verbatim.
|
|
390
|
+
*/
|
|
391
|
+
const compactionGraphSegment: StatusLineSegment = {
|
|
392
|
+
id: "compaction_graph",
|
|
393
|
+
render(ctx) {
|
|
394
|
+
const view = ctx.compaction;
|
|
395
|
+
if (!view) return INVISIBLE;
|
|
396
|
+
const { showBar, showStats } = ctx.options.context;
|
|
397
|
+
const parts: string[] = [];
|
|
398
|
+
if (showBar) parts.push(view.bar);
|
|
399
|
+
if (showStats) parts.push(theme.fg("dim", formatCompactionTokens(view)));
|
|
400
|
+
if (parts.length === 0) return INVISIBLE;
|
|
401
|
+
return { content: parts.join(" "), visible: true };
|
|
402
|
+
},
|
|
403
|
+
};
|
|
404
|
+
|
|
335
405
|
const accentAnsi = new Map<string, string>();
|
|
336
406
|
|
|
337
407
|
const sessionNameSegment: StatusLineSegment = {
|
|
@@ -366,6 +436,8 @@ export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
|
|
|
366
436
|
context_graph: contextGraphSegment,
|
|
367
437
|
scroll_hint: scrollHintSegment,
|
|
368
438
|
working: workingSegment,
|
|
439
|
+
compaction_info: compactionInfoSegment,
|
|
440
|
+
compaction_graph: compactionGraphSegment,
|
|
369
441
|
};
|
|
370
442
|
|
|
371
443
|
export function renderSegment(id: StatusLineSegmentId, ctx: SegmentContext): RenderedSegment {
|