@underactive/pi-topping-statusline 0.2.8 → 0.3.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.
@@ -0,0 +1,318 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
4
+ import {
5
+ CACHE_HIT_DEFAULT_CELLS,
6
+ CACHE_HIT_MAX_CELLS,
7
+ CACHE_HIT_MIN_CELLS,
8
+ clampCacheHitCells,
9
+ getCacheHitGraphCells,
10
+ getCacheHitPercent,
11
+ getCacheHitThresholds,
12
+ parseCacheHitStat,
13
+ renderCacheHitGraph,
14
+ stripCacheHitStat,
15
+ } from "../cache-hit.ts";
16
+ import { SegmentContextBuilder } from "../context.ts";
17
+ import { stripAnsi } from "../footer.ts";
18
+ import { buildStatusLine } from "../layout.ts";
19
+ import { SEGMENTS } from "../segments.ts";
20
+ import { resolveEffectiveSettings } from "../settings.ts";
21
+ import { buildSections, valuesToSettings } from "../settings-menu.ts";
22
+ import { theme } from "../theme.ts";
23
+ import type { SegmentContext } from "../types.ts";
24
+
25
+ const usage = (input: number, cacheRead: number, cacheWrite = 0) => ({
26
+ input,
27
+ output: 0,
28
+ cacheRead,
29
+ cacheWrite,
30
+ totalTokens: input + cacheRead + cacheWrite,
31
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
32
+ });
33
+
34
+ const entry = (value: unknown): SessionEntry => value as SessionEntry;
35
+
36
+ test("the fallback rate is pi's CH: the latest assistant response only", () => {
37
+ const entries = [
38
+ entry({ type: "message", message: { role: "assistant", usage: usage(80, 20) } }),
39
+ entry({ type: "message", message: { role: "assistant", usage: usage(6, 990, 4) } }),
40
+ entry({ type: "usage", usage: usage(500, 0) }),
41
+ entry({ type: "compaction", usage: usage(500, 0) }),
42
+ ];
43
+ assert.equal(getCacheHitPercent(entries), 99);
44
+ });
45
+
46
+ test("the fallback rate stays absent before an assistant reports prompt tokens", () => {
47
+ assert.equal(getCacheHitPercent([entry({ type: "usage", usage: usage(10, 90) })]), null);
48
+ assert.equal(getCacheHitPercent([entry({ type: "message", message: { role: "assistant", usage: usage(0, 0) } })]), null);
49
+ });
50
+
51
+ test("pi's CH stat is parsed from its stats line", () => {
52
+ assert.equal(parseCacheHitStat("↑1.2M ↓48K R6.1M W120K CH99.4% $2.357"), 99.4);
53
+ assert.equal(parseCacheHitStat("↑1.2M ↓48K $2.357"), null);
54
+ assert.equal(parseCacheHitStat(undefined), null);
55
+ });
56
+
57
+ test("99.1% fills four cells and 99.4% fills all five at five segments", () => {
58
+ assert.equal(getCacheHitGraphCells(99.1, 5), 4);
59
+ assert.equal(getCacheHitGraphCells(99.4, 5), 5);
60
+ });
61
+
62
+ test("the Cache hit bar toggle controls a standalone segment", () => {
63
+ const on = resolveEffectiveSettings({});
64
+ assert.equal(on.includes.cacheHit, true);
65
+ assert.ok(on.bottomRightSegments.includes("cache_hit_graph"));
66
+
67
+ const off = resolveEffectiveSettings({ segments: { cacheHitBar: false } });
68
+ assert.equal(off.includes.cacheHit, false);
69
+ assert.equal(off.segmentOptions.context.showBar, true);
70
+ assert.ok(!off.bottomRightSegments.includes("cache_hit_graph"));
71
+
72
+ const cacheOnly = resolveEffectiveSettings({ segments: { piStats: false, contextBar: false, contextStats: false } });
73
+ assert.deepEqual(cacheOnly.bottomRightSegments.slice(-1), ["cache_hit_graph"]);
74
+ });
75
+
76
+ test("the cache graph owns its CH label", () => {
77
+ const options = resolveEffectiveSettings({}).segmentOptions;
78
+ const result = SEGMENTS.cache_hit_graph.render({ options, cacheHitPercent: 100 } as SegmentContext);
79
+ assert.equal(stripAnsi(result.content), `${"▋".repeat(4)} CH100.0%`);
80
+ assert.equal(SEGMENTS.cache_hit_graph.render({ options, cacheHitPercent: null } as SegmentContext).visible, false);
81
+ });
82
+
83
+ test("the cache graph is as long as the CH bar segments setting", () => {
84
+ for (const cells of [4, 7, 10]) {
85
+ const options = resolveEffectiveSettings({ cacheHitBarSegments: cells }).segmentOptions;
86
+ const result = SEGMENTS.cache_hit_graph.render({ options, cacheHitPercent: 100 } as SegmentContext);
87
+ assert.equal(stripAnsi(result.content), `${"▋".repeat(cells)} CH100.0%`);
88
+ }
89
+ });
90
+
91
+ test("the builder prefers pi's CH stat and falls back to the derived rate", () => {
92
+ const effective = resolveEffectiveSettings({});
93
+ const fakePi = { exec: async () => undefined, getThinkingLevel: () => "off" } as unknown as ExtensionAPI;
94
+ const entries = [entry({ type: "message", message: { role: "assistant", usage: usage(50, 50) } })];
95
+ const ctx = {
96
+ cwd: "/tmp/pi-topping-statusline-tests",
97
+ model: undefined,
98
+ getContextUsage: () => undefined,
99
+ sessionManager: { getSessionName: () => undefined, getEntries: () => entries },
100
+ } as unknown as ExtensionContext;
101
+ const include = { ...effective.includes, git: false, pr: false, piStats: true, feeds: [] };
102
+
103
+ let stats: string | undefined = "↑1.2M CH99.4% $2.357";
104
+ const builder = new SegmentContextBuilder(fakePi);
105
+ builder.setPiStatsProvider(() => stats);
106
+ builder.attach(ctx);
107
+ const fromStats = builder.build(80, effective.segmentOptions, include, undefined);
108
+ assert.equal(fromStats.cacheHitPercent, 99.4);
109
+ assert.equal(fromStats.piStats, "↑1.2M $2.357", "CH moves beside its graph");
110
+
111
+ stats = undefined;
112
+ const fallback = new SegmentContextBuilder(fakePi);
113
+ fallback.setPiStatsProvider(() => stats);
114
+ fallback.attach(ctx);
115
+ assert.equal(fallback.build(80, effective.segmentOptions, include, undefined).cacheHitPercent, 50);
116
+ builder.dispose();
117
+ fallback.dispose();
118
+ });
119
+
120
+ test("the builder does not derive a CH rate when pi's stats line omits it", () => {
121
+ const effective = resolveEffectiveSettings({});
122
+ const fakePi = { exec: async () => undefined, getThinkingLevel: () => "off" } as unknown as ExtensionAPI;
123
+ const entries = [entry({ type: "message", message: { role: "assistant", usage: usage(100, 0) } })];
124
+ const ctx = {
125
+ cwd: "/tmp/pi-topping-statusline-tests",
126
+ model: undefined,
127
+ getContextUsage: () => undefined,
128
+ sessionManager: { getSessionName: () => undefined, getEntries: () => entries },
129
+ } as unknown as ExtensionContext;
130
+ const include = { ...effective.includes, git: false, pr: false, piStats: true, feeds: [] };
131
+ const builder = new SegmentContextBuilder(fakePi);
132
+ builder.setPiStatsProvider(() => "↑1.2M ↓48K $2.357");
133
+ builder.attach(ctx);
134
+ assert.equal(builder.build(80, effective.segmentOptions, include, undefined).cacheHitPercent, null);
135
+ builder.dispose();
136
+ });
137
+
138
+ test("only 0% is empty; after 50% each cell quarters the remaining miss rate", () => {
139
+ assert.equal(getCacheHitGraphCells(0, 5), 0);
140
+ assert.equal(getCacheHitGraphCells(0.1, 5), 1);
141
+ assert.equal(getCacheHitGraphCells(49.9, 5), 1);
142
+ assert.equal(getCacheHitGraphCells(50, 5), 2);
143
+ assert.equal(getCacheHitGraphCells(87.4, 5), 2);
144
+ assert.equal(getCacheHitGraphCells(87.5, 5), 3);
145
+ assert.equal(getCacheHitGraphCells(96.875, 5), 4);
146
+ assert.equal(getCacheHitGraphCells(99.21875, 5), 5);
147
+ assert.equal(getCacheHitGraphCells(100, 5), 5);
148
+ assert.equal(getCacheHitGraphCells(Number.NaN, 5), 0);
149
+ });
150
+
151
+ test("the rendered bar is five cells wide at five segments", () => {
152
+ assert.equal(stripAnsi(renderCacheHitGraph(0, 5)), "░".repeat(5));
153
+ assert.equal(stripAnsi(renderCacheHitGraph(10, 5)), `▋${"░".repeat(4)}`);
154
+ assert.equal(stripAnsi(renderCacheHitGraph(50, 5)), `${"▋".repeat(2)}${"░".repeat(3)}`);
155
+ });
156
+
157
+ test("the entire filled bar uses its current hit-rate color", () => {
158
+ assert.ok(renderCacheHitGraph(10).includes(theme.getFgAnsi("error")));
159
+
160
+ const sixty = renderCacheHitGraph(60);
161
+ assert.ok(sixty.includes(theme.getFgAnsi("statusLineContextElevated")));
162
+ assert.ok(!sixty.includes(theme.getFgAnsi("error")));
163
+
164
+ const ninetyFive = renderCacheHitGraph(95);
165
+ assert.ok(ninetyFive.includes(theme.getFgAnsi("warning")));
166
+ assert.ok(!ninetyFive.includes(theme.getFgAnsi("error")));
167
+ assert.ok(!ninetyFive.includes(theme.getFgAnsi("statusLineContextElevated")));
168
+
169
+ assert.ok(renderCacheHitGraph(99.1).includes(theme.getFgAnsi("statusLineCacheHit")));
170
+
171
+ const perfect = renderCacheHitGraph(100);
172
+ assert.equal(stripAnsi(perfect), "▋".repeat(4));
173
+ assert.ok(perfect.includes(theme.getFgAnsi("statusLineCacheHit")));
174
+ assert.ok(!perfect.includes(theme.getFgAnsi("warning")));
175
+ });
176
+
177
+ test("cache hit stat stripping preserves the other footer figures", () => {
178
+ assert.equal(stripCacheHitStat("↑425k ↓40k R11M W328k CH99.1% $5.091"), "↑425k ↓40k R11M W328k $5.091");
179
+ assert.equal(stripCacheHitStat("↑425k $5.091"), "↑425k $5.091");
180
+ });
181
+
182
+ test("the bottom bar groups Pi stats, the CH graph and context graph separately", () => {
183
+ const effective = resolveEffectiveSettings({ transparent: true, separator: "powerline-thin", cacheHitBarSegments: 5 });
184
+ const ctx = {
185
+ options: effective.segmentOptions,
186
+ model: undefined,
187
+ thinkingLevel: "off",
188
+ cwd: "/tmp",
189
+ sessionName: undefined,
190
+ contextPercent: 17.8,
191
+ contextTokens: 178_000,
192
+ contextWindow: 1_000_000,
193
+ cacheHitPercent: 99.1,
194
+ git: { branch: null, status: null, pr: null },
195
+ worktree: null,
196
+ scrollHint: undefined,
197
+ piStats: "↑425k ↓40k R11M W328k $5.091",
198
+ tokenRate: undefined,
199
+ feedData: undefined,
200
+ } satisfies SegmentContext;
201
+ const bar = buildStatusLine(0, ctx, effective, text => text, { left: [], right: effective.bottomRightSegments });
202
+ assert.equal(
203
+ stripAnsi(bar),
204
+ ` ↑425k ↓40k R11M W328k $5.091  ${"▋".repeat(4)}░ CH99.1%  ${"▋".repeat(4)}${"░".repeat(16)} 17.8%/1.0M `,
205
+ );
206
+
207
+ let fittedRight: readonly string[] = [];
208
+ const narrowBar = buildStatusLine(
209
+ 50,
210
+ ctx,
211
+ effective,
212
+ text => text,
213
+ { left: [], right: effective.bottomRightSegments },
214
+ undefined,
215
+ { onFit: fit => { fittedRight = fit.right; } },
216
+ );
217
+ assert.deepEqual(fittedRight, ["pi_stats", "cache_hit_graph"]);
218
+ assert.ok(stripAnsi(narrowBar).includes("CH99.1%"));
219
+ });
220
+
221
+ test("the 5-segment scale is the quartering scale: 50, 87.5, 96.875, 99.21875", () => {
222
+ assert.deepEqual(getCacheHitThresholds(5), [50, 87.5, 96.875, 99.21875]);
223
+ assert.deepEqual(getCacheHitThresholds(4), [50, 93.75, 99.21875]);
224
+ });
225
+
226
+ test("every bar size is logarithmic, from 50% at the second cell to 99.21875% at the last", () => {
227
+ for (let cells = CACHE_HIT_MIN_CELLS; cells <= CACHE_HIT_MAX_CELLS; cells++) {
228
+ const thresholds = getCacheHitThresholds(cells);
229
+ assert.equal(thresholds.length, cells - 1, `${cells} cells: one threshold per cell after the first`);
230
+ assert.equal(thresholds[0], 50);
231
+ assert.equal(thresholds.at(-1), 99.21875);
232
+ const misses = thresholds.map(t => 100 - t);
233
+ for (let i = 1; i < misses.length; i++) {
234
+ assert.ok(misses[i]! < misses[i - 1]!, `${cells} cells: thresholds strictly increase`);
235
+ }
236
+ // A log scale shrinks the miss rate by the same factor from cell to cell.
237
+ const ratios = misses.slice(1).map((miss, i) => miss / misses[i]!);
238
+ for (const ratio of ratios) assert.ok(Math.abs(ratio - ratios[0]!) < 1e-4, `${cells} cells: constant ratio ${ratios}`);
239
+ }
240
+ });
241
+
242
+ test("every bar size keeps the rules: 0% is empty, anything above fills a cell, 100% is full", () => {
243
+ for (let cells = CACHE_HIT_MIN_CELLS; cells <= CACHE_HIT_MAX_CELLS; cells++) {
244
+ assert.equal(getCacheHitGraphCells(0, cells), 0);
245
+ assert.equal(getCacheHitGraphCells(0.01, cells), 1);
246
+ assert.equal(getCacheHitGraphCells(49.9, cells), 1);
247
+ assert.equal(getCacheHitGraphCells(50, cells), 2);
248
+ assert.equal(getCacheHitGraphCells(100, cells), cells);
249
+ let previous = 0;
250
+ for (const percent of [1, 20, 50, 70, 90, 95, 98, 99, 99.5, 100]) {
251
+ const filled = getCacheHitGraphCells(percent, cells);
252
+ assert.ok(filled >= previous && filled <= cells, `${cells} cells at ${percent}%: ${filled}`);
253
+ previous = filled;
254
+ }
255
+ assert.equal(stripAnsi(renderCacheHitGraph(0, cells)), "░".repeat(cells));
256
+ assert.equal(stripAnsi(renderCacheHitGraph(100, cells)), "▋".repeat(cells));
257
+ }
258
+ });
259
+
260
+ test("more segments resolve finer near 100%", () => {
261
+ assert.equal(getCacheHitGraphCells(99.1, 3), 2);
262
+ assert.equal(getCacheHitGraphCells(99.4, 3), 3);
263
+ assert.equal(getCacheHitGraphCells(99.1, 4), 3);
264
+ assert.equal(getCacheHitGraphCells(99.1, 5), 4);
265
+ assert.equal(getCacheHitGraphCells(99.1, 10), 9, "98.69% is the ninth cell's threshold; the tenth needs 99.22%");
266
+ assert.equal(getCacheHitGraphCells(99.4, 10), 10);
267
+ });
268
+
269
+ test("a hit rate keeps its color at every bar size", () => {
270
+ for (const percent of [10, 60, 95, 99.1]) {
271
+ const colors = new Set<string>();
272
+ for (let cells = CACHE_HIT_MIN_CELLS; cells <= CACHE_HIT_MAX_CELLS; cells++) {
273
+ colors.add(renderCacheHitGraph(percent, cells).match(/^\x1b\[[\d;]+m/)![0]);
274
+ }
275
+ assert.equal(colors.size, 1, `${percent}% should be one color across sizes`);
276
+ }
277
+ });
278
+
279
+ test("the bar size is clamped to 3–10, with 4 for anything unusable", () => {
280
+ assert.equal(CACHE_HIT_DEFAULT_CELLS, 4);
281
+ assert.equal(clampCacheHitCells(2), 3);
282
+ assert.equal(clampCacheHitCells(3), 3);
283
+ assert.equal(clampCacheHitCells(11), 10);
284
+ assert.equal(clampCacheHitCells(6.4), 6);
285
+ assert.equal(clampCacheHitCells(Number.NaN), 4);
286
+ assert.equal(clampCacheHitCells(Number.POSITIVE_INFINITY), 4);
287
+ assert.equal(clampCacheHitCells("7"), 4);
288
+ assert.equal(clampCacheHitCells(undefined), 4);
289
+ assert.equal(renderCacheHitGraph(100, 99).length, renderCacheHitGraph(100, 10).length, "a bad size renders as the clamped size");
290
+ });
291
+
292
+ test("the CH bar segments setting resolves to the segment options, default 4", () => {
293
+ assert.equal(resolveEffectiveSettings({}).segmentOptions.cacheHit.cells, 4);
294
+ assert.equal(resolveEffectiveSettings({ cacheHitBarSegments: 8 }).segmentOptions.cacheHit.cells, 8);
295
+ assert.equal(resolveEffectiveSettings({ cacheHitBarSegments: 2 }).segmentOptions.cacheHit.cells, 3);
296
+ assert.equal(resolveEffectiveSettings({ cacheHitBarSegments: 40 }).segmentOptions.cacheHit.cells, 10);
297
+ });
298
+
299
+ test("the settings menu offers CH bar segments 3–10 under the Cache hit bar toggle and reads the choice back", () => {
300
+ const items = buildSections({}).flatMap(section => section.items);
301
+ const toggle = items.findIndex(item => item.id === "cacheHitBar");
302
+ const row = items[toggle + 1]!;
303
+ assert.equal(row.kind, "cycle");
304
+ if (row.kind !== "cycle") return;
305
+ assert.equal(row.id, "cacheHitBarSegments");
306
+ assert.equal(row.label, "CH bar segments");
307
+ assert.equal(row.value, "4", "defaults to 4");
308
+ assert.deepEqual(row.values, ["3", "4", "5", "6", "7", "8", "9", "10"]);
309
+
310
+ const saved = buildSections({ cacheHitBarSegments: 9 }).flatMap(section => section.items);
311
+ const savedRow = saved.find(item => item.id === "cacheHitBarSegments");
312
+ assert.equal(savedRow?.kind === "cycle" && savedRow.value, "9", "shows the saved size");
313
+
314
+ // Action rows hold no value, so only value-bearing rows seed the draft.
315
+ const values = Object.fromEntries(saved.flatMap(item => ("value" in item ? [[item.id, item.value]] : [])));
316
+ assert.equal(valuesToSettings({ ...values, cacheHitBarSegments: "7" }).cacheHitBarSegments, 7);
317
+ assert.equal(valuesToSettings({ ...values, cacheHitBarSegments: "garbage" }).cacheHitBarSegments, 4);
318
+ });
@@ -87,6 +87,26 @@ test("pi capability overrides take precedence over terminal hints", () => {
87
87
  }
88
88
  });
89
89
 
90
+ test("the working-group fade keeps the git indicators' hues in both colour modes", () => {
91
+ try {
92
+ forceTrueColor(false);
93
+ const untracked256 = theme.fadeAnsi(theme.fg("statusLineUntracked", "?1"), 0.9);
94
+ const dirty256 = theme.fadeAnsi(theme.fg("statusLineDirty", "*1"), 0.9);
95
+ forceTrueColor(true);
96
+ const untrackedTruecolor = theme.fadeAnsi(
97
+ theme.fg("statusLineUntracked", "?1"),
98
+ 0.9,
99
+ );
100
+ const actual = [untracked256, dirty256, untrackedTruecolor];
101
+ const details = `actual fades: ${JSON.stringify(actual)}`;
102
+ assert.ok(untracked256.startsWith("\x1b[38;5;39m?1"), details);
103
+ assert.ok(dirty256.startsWith("\x1b[38;5;178m*1"), details);
104
+ assert.ok(untrackedTruecolor.startsWith("\x1b[38;2;2;159;231m?1"), details);
105
+ } finally {
106
+ resetCapabilities();
107
+ }
108
+ });
109
+
90
110
  test("PI_TRUE_COLOR controls capability detection", () => {
91
111
  const previousTrueColor = process.env.PI_TRUE_COLOR;
92
112
  const previousHyperlinks = process.env.PI_HYPERLINKS;
@@ -1,58 +1,19 @@
1
1
  import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import { resetCapabilitiesCache, setCapabilities, setCapabilityOverrides } from "@earendil-works/pi-tui";
4
+ import { renderCometBar } from "@underactive/pi-topping-primitives/color";
4
5
  import { renderCompactionBar } from "../compaction-bar.ts";
5
6
 
6
- const MODE = { mode: "truecolor" as const };
7
- const ANSI_RE = /\x1b\[[0-9;]*m/g;
8
- const stripAnsi = (text: string) => text.replace(ANSI_RE, "");
7
+ // The bar's geometry, glyphs, clamping and 256-color path are tested where the renderer lives:
8
+ // @underactive/pi-topping-primitives tests/color-comet-bar.test.ts.
9
9
 
10
- function filledColors(bar: string): Array<[number, number, number]> {
11
- const colors: Array<[number, number, number]> = [];
12
- for (const match of bar.matchAll(/\x1b\[38;2;(\d+);(\d+);(\d+)m▋/g)) {
13
- colors.push([Number(match[1]), Number(match[2]), Number(match[3])]);
10
+ test("the compaction bar is the shared comet bar, with shimmerElapsedMs as its elapsed time", () => {
11
+ for (const mode of ["truecolor", "256color"] as const) {
12
+ assert.equal(
13
+ renderCompactionBar(50, { mode, shimmerElapsedMs: 500 }),
14
+ renderCometBar(50, { mode, elapsedMs: 500 }),
15
+ );
14
16
  }
15
- return colors;
16
- }
17
-
18
- function brightestCell(colors: Array<[number, number, number]>): number {
19
- let brightest = 0;
20
- for (let i = 1; i < colors.length; i++) {
21
- if ((colors[i]?.[1] ?? 0) > (colors[brightest]?.[1] ?? 0)) brightest = i;
22
- }
23
- return brightest;
24
- }
25
-
26
- test("bar has the expected glyphs, green trough, and foreground-only resets", () => {
27
- const bar = renderCompactionBar(50, MODE);
28
- assert.equal(stripAnsi(bar), `${"▋".repeat(10)}${"░".repeat(10)}`);
29
- assert.match(bar, /\x1b\[38;2;38;100;70m/);
30
- assert.match(bar, /\x1b\[2m/);
31
- assert.doesNotMatch(bar, /\x1b\[0m/);
32
- });
33
-
34
- test("the phosphor comet moves from right to left", () => {
35
- const right = filledColors(renderCompactionBar(100, { ...MODE, shimmerElapsedMs: 500 }));
36
- const left = filledColors(renderCompactionBar(100, { ...MODE, shimmerElapsedMs: 1_500 }));
37
- assert.equal(right.length, 20);
38
- assert.equal(left.length, 20);
39
- assert.notDeepEqual(right, left);
40
- assert.ok(brightestCell(right) > brightestCell(left));
41
- });
42
-
43
- test("bar supports 256-color escapes", () => {
44
- const bar = renderCompactionBar(100, { mode: "256color", shimmerElapsedMs: 500 });
45
- assert.match(bar, /\x1b\[38;5;\d+m/);
46
- assert.doesNotMatch(bar, /\x1b\[38;2;/);
47
- assert.doesNotMatch(bar, /\x1b\[0m/);
48
- });
49
-
50
- test("bar clamps NaN and out-of-range percentages", () => {
51
- const empty = renderCompactionBar(0, MODE);
52
- const full = renderCompactionBar(100, MODE);
53
- assert.equal(renderCompactionBar(Number.NaN, MODE), empty);
54
- assert.equal(renderCompactionBar(-10, MODE), empty);
55
- assert.equal(renderCompactionBar(150, MODE), full);
56
17
  });
57
18
 
58
19
  test("default bar color follows terminal capability detection", () => {
@@ -68,17 +68,21 @@ test("the bar hosts the progress only with the setting on and a stand-in slot sh
68
68
  assert.equal(hostsCompactionProgress(graphOnly), true, "the context graph alone hosts the bar");
69
69
  const statsOnly = resolveEffectiveSettings({ segments: { contextBar: false, contextStats: false } });
70
70
  assert.equal(hostsCompactionProgress(statsOnly), true, "pi stats alone host the figures");
71
- const nowhere = resolveEffectiveSettings({ segments: { piStats: false, contextBar: false, contextStats: false } });
71
+ const nowhere = resolveEffectiveSettings({ segments: { piStats: false, cacheHitBar: false, contextBar: false, contextStats: false } });
72
72
  assert.equal(hostsCompactionProgress(nowhere), false);
73
+ const cacheOnly = resolveEffectiveSettings({ segments: { piStats: false, contextBar: false, contextStats: false } });
74
+ assert.ok(cacheOnly.bottomRightSegments.includes("cache_hit_graph"), "the cache bar keeps its segment");
75
+ assert.equal(hostsCompactionProgress(cacheOnly), false, "the cache bar alone is not a stand-in slot");
73
76
  });
74
77
 
75
78
  test("bottomRightSegments swaps in the stand-ins in place while compacting", () => {
76
79
  const effective = resolveEffectiveSettings({ segments: { feeds: true, tokenRate: true } });
77
- assert.deepEqual(effective.bottomRightSegments, ["feeds", "token_rate", "pi_stats", "context_graph"]);
80
+ assert.deepEqual(effective.bottomRightSegments, ["feeds", "token_rate", "pi_stats", "cache_hit_graph", "context_graph"]);
78
81
  assert.deepEqual(bottomRightSegments(effective, true), [
79
82
  "feeds",
80
83
  "token_rate",
81
84
  "compaction_info",
85
+ "cache_hit_graph",
82
86
  "compaction_graph",
83
87
  ]);
84
88
  assert.equal(bottomRightSegments(effective, false), effective.bottomRightSegments);
@@ -21,7 +21,7 @@ import type { SegmentContext } from "../types.ts";
21
21
 
22
22
  const WINDOW = 131_072;
23
23
  const OPTIONS = resolveEffectiveSettings({}).segmentOptions;
24
- const INCLUDE = { git: false, pr: false, piStats: false, tokenRate: false, feeds: [] };
24
+ const INCLUDE = { git: false, pr: false, piStats: false, tokenRate: false, cacheHit: false, feeds: [] };
25
25
  /** Outside any repo, so the builder's git probes stay inert. */
26
26
  const CWD = "/tmp/pi-topping-statusline-tests";
27
27
  const fakePi = { exec: async () => undefined, getThinkingLevel: () => "off" } as unknown as ExtensionAPI;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Contracts for the feeds segment, which surfaces values other extensions
3
- * publish as custom session entries. The default subscription targets
4
- * pi-prompt-cache's savings feed, whose author specified the currency rules.
3
+ * publish as custom session entries. The currency rules are those of
4
+ * pi-topping-compact's savings feed, whose author specified them. No
5
+ * subscription ships by default, so every test here passes its feeds explicitly.
5
6
  *
6
7
  * Ordering matters here: a publisher emits at turn end or after compaction,
7
8
  * both of which require a completed model call, so a live entry is always
@@ -22,9 +23,12 @@ import { theme } from "../theme.ts";
22
23
  import type { FeedDisplayState, SegmentContext, StatusLineFeed } from "../types.ts";
23
24
  import { feedKey } from "../utils.ts";
24
25
 
25
- const SAVINGS = "pi-prompt-cache/savings";
26
- const OPTIONS = resolveEffectiveSettings({}).segmentOptions;
27
- const INCLUDE = { git: false, pr: false, piStats: false, tokenRate: false, feeds: [SAVINGS] };
26
+ const SAVINGS = "pi-topping-compact/savings";
27
+ /** The subscription the old default seeded, passed explicitly now that no default ships. */
28
+ const OPTIONS = resolveEffectiveSettings({
29
+ feeds: [{ customType: SAVINGS, field: "savedUsd", prefix: "CS", format: "currency" }],
30
+ }).segmentOptions;
31
+ const INCLUDE = { git: false, pr: false, piStats: false, tokenRate: false, cacheHit: false, feeds: [SAVINGS] };
28
32
  /** Outside any repo, so the builder's git probes stay inert. */
29
33
  const CWD = "/tmp/pi-topping-statusline-tests";
30
34
  /** Match the builder's intentional prototype-less feed map in deep equality assertions. */
@@ -56,13 +60,14 @@ const feedEntry = (data: unknown, customType = SAVINGS, timestamp = new Date().t
56
60
  async function contextAfterEmitting(
57
61
  makeEntries: () => SessionEntry[],
58
62
  include = INCLUDE,
63
+ options = OPTIONS,
59
64
  ): Promise<SegmentContext> {
60
65
  const entries: SessionEntry[] = [];
61
66
  const builder = new SegmentContextBuilder(fakePi);
62
67
  builder.attach(fakeCtx(entries));
63
68
  await sleep(3);
64
69
  entries.push(...makeEntries());
65
- return builder.build(80, OPTIONS, include, undefined);
70
+ return builder.build(80, options, include, undefined);
66
71
  }
67
72
 
68
73
  const BASE = await contextAfterEmitting(() => []);
@@ -456,23 +461,36 @@ test("the prefix is literal, so the currency symbol can live in either place", (
456
461
 
457
462
  // ── settings ───────────────────────────────────────────────────────────────
458
463
 
459
- test("defaults: segment off, seeded with the pi-prompt-cache subscription", () => {
464
+ test("defaults: segment off, no subscriptions", () => {
460
465
  assert.equal(DEFAULT_SEGMENTS.feeds, false);
461
466
  assert.ok(!resolveEffectiveSettings({}).bottomRightSegments.includes("feeds"));
462
- assert.deepEqual(resolveEffectiveSettings({}).segmentOptions.feeds, [
463
- { customType: SAVINGS, field: "savedUsd", prefix: "CS", format: "currency" },
464
- ]);
467
+ assert.deepEqual(resolveEffectiveSettings({}).segmentOptions.feeds, []);
465
468
  assert.deepEqual(resolveEffectiveSettings({ segments: { feeds: true } }).bottomRightSegments, [
466
469
  "feeds",
467
470
  "pi_stats",
471
+ "cache_hit_graph",
468
472
  "context_graph",
469
473
  ]);
470
474
  });
471
475
 
476
+ test("a saved subscription renders a fresh entry as CS currency", async () => {
477
+ const { segmentOptions } = resolveEffectiveSettings({ feeds: [savingsFeed("currency", "CS")] });
478
+ const include = { ...INCLUDE, feeds: segmentOptions.feeds.map(feed => feed.customType) };
479
+ const ctx = await contextAfterEmitting(() => [feedEntry({ savedUsd: 1.44 })], include, segmentOptions);
480
+ assert.equal(stripAnsi(SEGMENTS.feeds.render(ctx).content), "CS$1.44");
481
+ });
482
+
483
+ test("an entry nobody subscribed to renders nothing under the default settings", async () => {
484
+ const { segmentOptions } = resolveEffectiveSettings({});
485
+ const include = { ...INCLUDE, feeds: segmentOptions.feeds.map(feed => feed.customType) };
486
+ const ctx = await contextAfterEmitting(() => [feedEntry({ savedUsd: 1.44 })], include, segmentOptions);
487
+ assert.equal(SEGMENTS.feeds.render(ctx).visible, false);
488
+ });
489
+
472
490
  test("feeds lead the bottom-right group", () => {
473
491
  assert.deepEqual(
474
492
  resolveEffectiveSettings({ segments: { feeds: true, tokenRate: true } }).bottomRightSegments,
475
- ["feeds", "token_rate", "pi_stats", "context_graph"],
493
+ ["feeds", "token_rate", "pi_stats", "cache_hit_graph", "context_graph"],
476
494
  );
477
495
  });
478
496
 
@@ -7,7 +7,7 @@ import { SEGMENTS } from "../segments.ts";
7
7
  import { resolveEffectiveSettings } from "../settings.ts";
8
8
 
9
9
  const OPTIONS = resolveEffectiveSettings({}).segmentOptions;
10
- const GIT_INCLUDE = { git: true, pr: false, piStats: false, tokenRate: false, feeds: [] };
10
+ const GIT_INCLUDE = { git: true, pr: false, piStats: false, tokenRate: false, cacheHit: false, feeds: [] };
11
11
  const NO_GIT_INCLUDE = { ...GIT_INCLUDE, git: false };
12
12
  const CWD = "/repo";
13
13
 
@@ -37,7 +37,7 @@ test("the context builder supplies changed-file counts to the git segment", asyn
37
37
  commands.push(args);
38
38
  if (args[0] === "rev-parse") return { code: 0, stdout: "/repo/.git\n/repo/.git\n/repo\n" };
39
39
  if (args[0] === "branch") return { code: 0, stdout: "main\n" };
40
- if (args[0] === "status") return { code: 0, stdout: " M unstaged.ts\nM staged.ts\n?? new.ts\n" };
40
+ if (args.includes("status")) return { code: 0, stdout: " M unstaged.ts\nM staged.ts\n?? new.ts\n" };
41
41
  return { code: 1, stdout: "" };
42
42
  },
43
43
  getThinkingLevel: () => "off",
@@ -57,6 +57,6 @@ test("the context builder supplies changed-file counts to the git segment", asyn
57
57
  untracked: 1,
58
58
  });
59
59
  assert.equal(stripAnsi(SEGMENTS.git.render(snapshot).content).includes("main *1 +1 ?1"), true);
60
- assert.ok(commands.some(args => args.join(" ") === "status --porcelain"));
60
+ assert.ok(commands.some(args => args.join(" ") === "-c core.fsmonitor=false status --porcelain"));
61
61
  builder.dispose();
62
62
  });
@@ -4,19 +4,10 @@ import {
4
4
  NVIDIA_GREEN_DARK_HEX,
5
5
  NVIDIA_GREEN_HEX,
6
6
  NvidiaGreenBorder,
7
- isSwitchyardProvider,
8
7
  } from "../nvidia-green.ts";
9
8
  import { resolveEffectiveSettings } from "../settings.ts";
10
9
  import { detectColorMode, hexToFgAnsi } from "../theme.ts";
11
10
 
12
- test("recognizes the Switchyard provider case-insensitively after trimming", () => {
13
- assert.equal(isSwitchyardProvider("switchyard"), true);
14
- assert.equal(isSwitchyardProvider(" Switchyard "), true);
15
- assert.equal(isSwitchyardProvider(undefined), false);
16
- assert.equal(isSwitchyardProvider(""), false);
17
- assert.equal(isSwitchyardProvider("other"), false);
18
- });
19
-
20
11
  test("switchyard green settings default to true and resolve explicit false", () => {
21
12
  const defaults = resolveEffectiveSettings({});
22
13
  assert.equal(defaults.nvidiaGreenBorder, true);
@@ -25,7 +25,7 @@ builder.attach(fakeCtx);
25
25
  const BASE = builder.build(
26
26
  80,
27
27
  resolveEffectiveSettings({}).segmentOptions,
28
- { git: false, pr: false, piStats: false, tokenRate: false, feeds: [] },
28
+ { git: false, pr: false, piStats: false, tokenRate: false, cacheHit: false, feeds: [] },
29
29
  undefined,
30
30
  );
31
31