@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.
- package/README.md +11 -10
- package/index.ts +3 -2
- package/package.json +8 -5
- package/src/cache-hit.ts +107 -0
- package/src/compaction-bar.ts +9 -49
- package/src/context-graph.ts +6 -3
- package/src/context.ts +29 -3
- package/src/footer.ts +2 -4
- package/src/nvidia-green.ts +14 -10
- package/src/rainbow.ts +3 -23
- package/src/segments.ts +13 -1
- package/src/settings-menu.ts +73 -41
- package/src/settings.ts +49 -40
- package/src/tests/cache-hit.test.ts +318 -0
- package/src/tests/color-mode.test.ts +20 -0
- package/src/tests/compaction-bar.test.ts +9 -48
- package/src/tests/compaction-embed.test.ts +6 -2
- package/src/tests/context-estimate.test.ts +1 -1
- package/src/tests/feeds.test.ts +29 -11
- package/src/tests/git-status.test.ts +3 -3
- package/src/tests/nvidia-green.test.ts +0 -9
- package/src/tests/rainbow.test.ts +1 -1
- package/src/tests/settings-menu.test.ts +179 -0
- package/src/tests/settings.test.ts +56 -0
- package/src/tests/token-rate.test.ts +1 -1
- package/src/tests/working-status.test.ts +8 -8
- package/src/tests/working.test.ts +1 -7
- package/src/theme.ts +24 -101
- package/src/token-rate.ts +16 -117
- package/src/types.ts +14 -5
- package/src/utils.ts +8 -17
- package/src/working-status-embed.ts +0 -7
- package/src/working-status.ts +4 -5
- package/src/menu.ts +0 -676
- package/src/tests/menu.test.ts +0 -138
package/src/settings-menu.ts
CHANGED
|
@@ -4,11 +4,18 @@
|
|
|
4
4
|
* menu values, and the command registration.
|
|
5
5
|
*/
|
|
6
6
|
import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
showMenu,
|
|
9
|
+
type MenuItem,
|
|
10
|
+
type MenuSection,
|
|
11
|
+
type MenuValue,
|
|
12
|
+
type PreviewSlot,
|
|
13
|
+
} from "@underactive/pi-topping-primitives/menu";
|
|
7
14
|
import { makeBoxPainters, renderBoxRow } from "./box.js";
|
|
15
|
+
import { CACHE_HIT_MAX_CELLS, CACHE_HIT_MIN_CELLS, clampCacheHitCells, stripCacheHitStat } from "./cache-hit.js";
|
|
8
16
|
import { compactionPreviewView } from "./compaction-embed.js";
|
|
9
17
|
import type { SegmentContextBuilder } from "./context.js";
|
|
10
18
|
import { buildStatusLine } from "./layout.js";
|
|
11
|
-
import { showMenu, type MenuSection, type MenuValue, type PreviewResult } from "./menu.js";
|
|
12
19
|
import { NvidiaGreenBorder, isSwitchyardProvider } from "./nvidia-green.js";
|
|
13
20
|
import { RAINBOW_CYCLE_MS, RAINBOW_FRAME_MS, RainbowBorder } from "./rainbow.js";
|
|
14
21
|
import {
|
|
@@ -42,6 +49,12 @@ const SYMBOL_FROM_LABEL: Record<string, SymbolPreset> = Object.fromEntries(
|
|
|
42
49
|
(Object.entries(SYMBOL_LABELS) as [SymbolPreset, string][]).map(([preset, label]) => [label, preset]),
|
|
43
50
|
);
|
|
44
51
|
|
|
52
|
+
/** Menu values for the CH bar length: the menu cycles strings, so each size is its decimal text. */
|
|
53
|
+
const CACHE_HIT_CELL_VALUES: readonly string[] = Array.from(
|
|
54
|
+
{ length: CACHE_HIT_MAX_CELLS - CACHE_HIT_MIN_CELLS + 1 },
|
|
55
|
+
(_, i) => String(CACHE_HIT_MIN_CELLS + i),
|
|
56
|
+
);
|
|
57
|
+
|
|
45
58
|
const SECTION_TITLES = [
|
|
46
59
|
"Top Left Segment Group",
|
|
47
60
|
"Top Right Segment Group",
|
|
@@ -66,6 +79,7 @@ const SEGMENT_ROWS: readonly {
|
|
|
66
79
|
{ id: "feeds", label: "Feeds", section: "Bottom Right Segment Group" },
|
|
67
80
|
{ id: "tokenRate", label: "Token rate", section: "Bottom Right Segment Group" },
|
|
68
81
|
{ id: "piStats", label: "Pi stats", section: "Bottom Right Segment Group" },
|
|
82
|
+
{ id: "cacheHitBar", label: "Cache hit bar", section: "Bottom Right Segment Group" },
|
|
69
83
|
{ id: "contextBar", label: "Context bar", section: "Bottom Right Segment Group" },
|
|
70
84
|
{ id: "contextStats", label: "Context stats", section: "Bottom Right Segment Group" },
|
|
71
85
|
{ id: "scrollHint", label: "Scroll hint", section: "Bottom Left Segment Group" },
|
|
@@ -73,8 +87,8 @@ const SEGMENT_ROWS: readonly {
|
|
|
73
87
|
{ id: "tokenRateBottomLeft", label: "Token rate", section: "Bottom Left Segment Group" },
|
|
74
88
|
];
|
|
75
89
|
|
|
76
|
-
// Feed rows carry a stable key (independent of on-screen position) so
|
|
77
|
-
//
|
|
90
|
+
// Feed rows carry a stable key (independent of on-screen position) so the
|
|
91
|
+
// menu's id-keyed value carry-over on a rebuild survives a removal.
|
|
78
92
|
const FEED_PREFIX = "feed";
|
|
79
93
|
const feedKey = (key: number, part: keyof StatusLineFeed) => `${FEED_PREFIX}.${key}.${part}`;
|
|
80
94
|
const ADD_FEED_ID = `${FEED_PREFIX}.add`;
|
|
@@ -90,18 +104,18 @@ interface FeedPair {
|
|
|
90
104
|
const pairsFromFeeds = (feeds: readonly StatusLineFeed[]): FeedPair[] => feeds.map((feed, key) => ({ key, feed }));
|
|
91
105
|
|
|
92
106
|
function feedSection(pairs: readonly FeedPair[]): MenuSection {
|
|
93
|
-
const items:
|
|
107
|
+
const items: MenuItem[] = [];
|
|
94
108
|
pairs.forEach(({ key, feed }, index) => {
|
|
95
109
|
const n = index + 1;
|
|
96
110
|
items.push(
|
|
97
|
-
{ id: feedKey(key, "customType"), label: `${n}. type`, value: feed.customType,
|
|
98
|
-
{ id: feedKey(key, "field"), label: `${n}. field`, value: feed.field,
|
|
99
|
-
{ id: feedKey(key, "prefix"), label: `${n}. prefix`, value: feed.prefix,
|
|
100
|
-
{ id: feedKey(key, "format"), label: `${n}. format`, value: feed.format,
|
|
101
|
-
{ id: removeFeedId(key), label: `${n}. remove this feed
|
|
111
|
+
{ kind: "text", id: feedKey(key, "customType"), label: `${n}. type`, value: feed.customType, placeholder: "ext/custom-type" },
|
|
112
|
+
{ kind: "text", id: feedKey(key, "field"), label: `${n}. field`, value: feed.field, placeholder: "fieldName" },
|
|
113
|
+
{ kind: "text", id: feedKey(key, "prefix"), label: `${n}. prefix`, value: feed.prefix, placeholder: "(none)" },
|
|
114
|
+
{ kind: "cycle", id: feedKey(key, "format"), label: `${n}. format`, value: feed.format, values: FEED_FORMATS },
|
|
115
|
+
{ kind: "action", id: removeFeedId(key), label: `${n}. remove this feed` },
|
|
102
116
|
);
|
|
103
117
|
});
|
|
104
|
-
items.push({ id: ADD_FEED_ID, label: "+ add feed"
|
|
118
|
+
items.push({ kind: "action", id: ADD_FEED_ID, label: "+ add feed" });
|
|
105
119
|
return { title: "Feeds", items };
|
|
106
120
|
}
|
|
107
121
|
|
|
@@ -133,51 +147,59 @@ function feedsFromValues(values: Record<string, MenuValue>): StatusLineFeed[] {
|
|
|
133
147
|
return feedPairsFromValues(values).map(({ feed }) => feed);
|
|
134
148
|
}
|
|
135
149
|
|
|
136
|
-
function buildSections(settings: StatusLineSettings): MenuSection[] {
|
|
150
|
+
export function buildSections(settings: StatusLineSettings): MenuSection[] {
|
|
137
151
|
const seg = { ...DEFAULT_SEGMENTS, ...settings.segments };
|
|
138
152
|
const global: MenuSection = {
|
|
139
153
|
title: "Global",
|
|
140
154
|
items: [
|
|
141
|
-
{ id: "transparent", label: "Transparent Segments", value: settings.transparent ?? true },
|
|
155
|
+
{ kind: "toggle", id: "transparent", label: "Transparent Segments", value: settings.transparent ?? true },
|
|
142
156
|
{
|
|
157
|
+
kind: "cycle",
|
|
143
158
|
id: "separator",
|
|
144
159
|
label: "Separator",
|
|
145
160
|
value: settings.separator ?? "powerline-thin",
|
|
146
|
-
|
|
161
|
+
values: SEPARATORS,
|
|
147
162
|
},
|
|
148
|
-
{ id: "symbols", label: "Symbols", value: SYMBOL_LABELS[settings.symbols ?? "nerd"],
|
|
163
|
+
{ kind: "cycle", id: "symbols", label: "Symbols", value: SYMBOL_LABELS[settings.symbols ?? "nerd"], values: SYMBOL_VALUES },
|
|
149
164
|
{
|
|
165
|
+
kind: "cycle",
|
|
150
166
|
id: "borderStyle",
|
|
151
167
|
label: "Border style",
|
|
152
168
|
value: settings.borderStyle ?? "rounded",
|
|
153
|
-
|
|
169
|
+
values: BORDER_STYLES,
|
|
154
170
|
},
|
|
155
171
|
{
|
|
172
|
+
kind: "toggle",
|
|
156
173
|
id: "rainbowBorder",
|
|
157
174
|
label: "Rainbow border on max thinking",
|
|
158
175
|
value: settings.rainbowBorder ?? true,
|
|
159
176
|
},
|
|
160
177
|
{
|
|
178
|
+
kind: "toggle",
|
|
161
179
|
id: "rainbowAnimation",
|
|
162
180
|
label: "Animate rainbow border",
|
|
163
181
|
value: settings.rainbowAnimation ?? true,
|
|
164
182
|
},
|
|
165
183
|
{
|
|
184
|
+
kind: "toggle",
|
|
166
185
|
id: "nvidiaGreenBorder",
|
|
167
186
|
label: "NVIDIA-green when using Switchyard",
|
|
168
187
|
value: settings.nvidiaGreenBorder ?? true,
|
|
169
188
|
},
|
|
170
189
|
{
|
|
190
|
+
kind: "toggle",
|
|
171
191
|
id: "nvidiaGreenAnimation",
|
|
172
192
|
label: "Animate Switchyard green border",
|
|
173
193
|
value: settings.nvidiaGreenAnimation ?? true,
|
|
174
194
|
},
|
|
175
195
|
{
|
|
196
|
+
kind: "toggle",
|
|
176
197
|
id: "embedWorkingStatus",
|
|
177
198
|
label: "Embed status spinners",
|
|
178
199
|
value: settings.embedWorkingStatus ?? false,
|
|
179
200
|
},
|
|
180
201
|
{
|
|
202
|
+
kind: "toggle",
|
|
181
203
|
id: "embedCompactionProgress",
|
|
182
204
|
label: "Embed compaction progress",
|
|
183
205
|
value: settings.embedCompactionProgress ?? true,
|
|
@@ -188,17 +210,27 @@ function buildSections(settings: StatusLineSettings): MenuSection[] {
|
|
|
188
210
|
global,
|
|
189
211
|
...SECTION_TITLES.map(title => ({
|
|
190
212
|
title,
|
|
191
|
-
items: SEGMENT_ROWS.filter(row => row.section === title).
|
|
192
|
-
id: row.id,
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
213
|
+
items: SEGMENT_ROWS.filter(row => row.section === title).flatMap((row): MenuItem[] => {
|
|
214
|
+
const item: MenuItem = { kind: "toggle", id: row.id, label: row.label, value: seg[row.id] };
|
|
215
|
+
// The bar's length sits right under its on/off toggle.
|
|
216
|
+
if (row.id !== "cacheHitBar") return [item];
|
|
217
|
+
return [
|
|
218
|
+
item,
|
|
219
|
+
{
|
|
220
|
+
kind: "cycle",
|
|
221
|
+
id: "cacheHitBarSegments",
|
|
222
|
+
label: "CH bar segments",
|
|
223
|
+
value: String(clampCacheHitCells(settings.cacheHitBarSegments)),
|
|
224
|
+
values: CACHE_HIT_CELL_VALUES,
|
|
225
|
+
},
|
|
226
|
+
];
|
|
227
|
+
}),
|
|
196
228
|
})),
|
|
197
229
|
feedSection(pairsFromFeeds(settings.feeds ? sanitizeFeeds(settings.feeds) : DEFAULT_FEEDS)),
|
|
198
230
|
];
|
|
199
231
|
}
|
|
200
232
|
|
|
201
|
-
function valuesToSettings(values: Record<string, MenuValue>): StatusLineSettings {
|
|
233
|
+
export function valuesToSettings(values: Record<string, MenuValue>): StatusLineSettings {
|
|
202
234
|
const segments: StatusLineSegmentToggles = {};
|
|
203
235
|
for (const row of SEGMENT_ROWS) segments[row.id] = values[row.id] === true;
|
|
204
236
|
const separator = values.separator as StatusLineSeparatorStyle;
|
|
@@ -216,6 +248,7 @@ function valuesToSettings(values: Record<string, MenuValue>): StatusLineSettings
|
|
|
216
248
|
nvidiaGreenAnimation: values.nvidiaGreenAnimation === true,
|
|
217
249
|
embedWorkingStatus: values.embedWorkingStatus === true,
|
|
218
250
|
embedCompactionProgress: values.embedCompactionProgress === true,
|
|
251
|
+
cacheHitBarSegments: clampCacheHitCells(Number(values.cacheHitBarSegments)),
|
|
219
252
|
};
|
|
220
253
|
}
|
|
221
254
|
|
|
@@ -226,7 +259,8 @@ const CANNED_TOKEN_RATE = { rate: 87, phase: "active", fadeShade: 0 } as const;
|
|
|
226
259
|
/** Stand-in payloads so configured feeds still preview without a live publisher. */
|
|
227
260
|
const CANNED_FEED_NUMBER = 1.23;
|
|
228
261
|
const CANNED_HINT = "↑ 3 more";
|
|
229
|
-
const
|
|
262
|
+
const CANNED_CACHE_HIT = 92.3;
|
|
263
|
+
const CANNED_STATS = `↑ 12.4K ↓ 3.1K R 148K W 12K CH${CANNED_CACHE_HIT}% $0.42`;
|
|
230
264
|
const CANNED_GIT = { branch: "main", status: { staged: 1, unstaged: 2, untracked: 3 } };
|
|
231
265
|
const CANNED_PERCENT = 42;
|
|
232
266
|
const CANNED_WINDOW = 200_000;
|
|
@@ -250,7 +284,8 @@ function cannedFeedData(
|
|
|
250
284
|
return data;
|
|
251
285
|
}
|
|
252
286
|
|
|
253
|
-
|
|
287
|
+
/** The menu's preview slot: three rows of the real box, sampled synchronously on every render. */
|
|
288
|
+
class StatusLinePreview implements PreviewSlot<Record<string, MenuValue>> {
|
|
254
289
|
readonly #builder: SegmentContextBuilder;
|
|
255
290
|
readonly #uiTheme: Theme;
|
|
256
291
|
|
|
@@ -261,14 +296,12 @@ class StatusLinePreview {
|
|
|
261
296
|
|
|
262
297
|
render(
|
|
263
298
|
values: Record<string, MenuValue>,
|
|
264
|
-
elapsedMs: number,
|
|
265
|
-
|
|
266
|
-
innerWidth: number,
|
|
267
|
-
): PreviewResult {
|
|
299
|
+
{ elapsedMs, activeItemId, width }: { elapsedMs: number; activeItemId: string | undefined; width: number },
|
|
300
|
+
): { lines: string[]; nextRefreshInMs?: number } {
|
|
268
301
|
const effective = resolveEffectiveSettings(valuesToSettings(values));
|
|
269
|
-
//
|
|
270
|
-
// 6-cell chrome budget for the bars
|
|
271
|
-
const boxWidth = Math.max(24,
|
|
302
|
+
// The menu hands over its full inner width and adds no margin, so the one-cell
|
|
303
|
+
// indent is added here; mirror renderBoxed's 6-cell chrome budget for the bars.
|
|
304
|
+
const boxWidth = Math.max(24, width - 1);
|
|
272
305
|
const barWidth = boxWidth - 6;
|
|
273
306
|
|
|
274
307
|
// The theme singleton drives glyph lookups everywhere, so swap the
|
|
@@ -281,6 +314,7 @@ class StatusLinePreview {
|
|
|
281
314
|
pr: true,
|
|
282
315
|
piStats: true,
|
|
283
316
|
tokenRate: true,
|
|
317
|
+
cacheHit: effective.includes.cacheHit,
|
|
284
318
|
feeds: effective.segmentOptions.feeds.map(f => f.customType),
|
|
285
319
|
};
|
|
286
320
|
const base = this.#builder.build(barWidth, effective.segmentOptions, include, CANNED_HINT);
|
|
@@ -302,7 +336,9 @@ class StatusLinePreview {
|
|
|
302
336
|
contextPercent: base.contextPercent ?? CANNED_PERCENT,
|
|
303
337
|
contextTokens: base.contextPercent == null ? Math.round((contextWindow * CANNED_PERCENT) / 100) : base.contextTokens,
|
|
304
338
|
contextWindow,
|
|
305
|
-
|
|
339
|
+
cacheHitPercent: base.cacheHitPercent ?? CANNED_CACHE_HIT,
|
|
340
|
+
// The canned line stands in for pi's, so it moves CH beside the graph exactly as the builder does.
|
|
341
|
+
piStats: base.piStats ?? (effective.includes.cacheHit ? stripCacheHitStat(CANNED_STATS) : CANNED_STATS),
|
|
306
342
|
tokenRate: base.tokenRate ?? CANNED_TOKEN_RATE,
|
|
307
343
|
feedData: cannedFeedData(effective.segmentOptions.feeds, base.feedData),
|
|
308
344
|
};
|
|
@@ -356,7 +392,7 @@ class StatusLinePreview {
|
|
|
356
392
|
" ".repeat(Math.max(0, boxWidth - 2)) +
|
|
357
393
|
painters.paint(1, boxWidth - 1, box.vertical),
|
|
358
394
|
renderBoxRow(painters, bottom, bottomIdx, boxWidth, box.bottomLeft, box.bottomRight),
|
|
359
|
-
],
|
|
395
|
+
].map(line => ` ${line}`),
|
|
360
396
|
// A static colorizer stays at phase zero and needs no repaint timer.
|
|
361
397
|
nextRefreshInMs: animationOn ? RAINBOW_FRAME_MS : undefined,
|
|
362
398
|
};
|
|
@@ -379,12 +415,11 @@ export function registerSettingsCommand(
|
|
|
379
415
|
ctx.ui.notify("/topping-statusline-settings requires TUI mode", "error");
|
|
380
416
|
return;
|
|
381
417
|
}
|
|
382
|
-
const preview = new StatusLinePreview(builder, ctx.ui.theme);
|
|
383
418
|
const result = await showMenu<Record<string, MenuValue>>(ctx, {
|
|
384
419
|
title: "Pi Topping Statusline: Settings",
|
|
385
420
|
sections: buildSections(state.settings),
|
|
386
421
|
hints: ["↑↓ move", "←→ cycle", "␣ toggle", "⏎ apply/edit", "esc cancel"],
|
|
387
|
-
preview:
|
|
422
|
+
preview: new StatusLinePreview(builder, ctx.ui.theme),
|
|
388
423
|
onAction: (id, values) => {
|
|
389
424
|
const pairs = feedPairsFromValues(values);
|
|
390
425
|
let nextPairs: FeedPair[];
|
|
@@ -393,7 +428,7 @@ export function registerSettingsCommand(
|
|
|
393
428
|
nextPairs = [...pairs, { key, feed: { customType: "", field: "", prefix: "", format: "currency" } }];
|
|
394
429
|
} else {
|
|
395
430
|
const match = REMOVE_FEED_RE.exec(id);
|
|
396
|
-
if (!match) return
|
|
431
|
+
if (!match) return;
|
|
397
432
|
const removedKey = Number(match[1]);
|
|
398
433
|
nextPairs = pairs.filter(p => p.key !== removedKey);
|
|
399
434
|
}
|
|
@@ -402,16 +437,13 @@ export function registerSettingsCommand(
|
|
|
402
437
|
return [...buildSections(valuesToSettings(values)).slice(0, -1), feedSection(nextPairs)];
|
|
403
438
|
},
|
|
404
439
|
});
|
|
440
|
+
// Escape hands back the un-applied draft, so the values are only read on apply.
|
|
405
441
|
if (!result.applied) return;
|
|
406
442
|
const next = valuesToSettings(result.values);
|
|
407
443
|
applySettings(state, next);
|
|
408
444
|
onChange();
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
} catch (err) {
|
|
412
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
413
|
-
ctx.ui.notify(`Failed to save statusline settings: ${message}`, "error");
|
|
414
|
-
}
|
|
445
|
+
const saved = saveSettings(next);
|
|
446
|
+
if (!saved.ok) ctx.ui.notify(`Failed to save statusline settings: ${saved.message}`, "error");
|
|
415
447
|
},
|
|
416
448
|
});
|
|
417
449
|
}
|
package/src/settings.ts
CHANGED
|
@@ -3,9 +3,8 @@
|
|
|
3
3
|
* statusline settings live in a small JSON at
|
|
4
4
|
* ~/.pi/agent/pi-topping-statusline/settings.json.
|
|
5
5
|
*/
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { createSettingsStore, sanitize, toppingSettingsPath, type SettingsWriteResult } from "@underactive/pi-topping-primitives/settings";
|
|
7
|
+
import { clampCacheHitCells } from "./cache-hit.js";
|
|
9
8
|
import { theme, type BorderStyle, type SymbolPreset } from "./theme.js";
|
|
10
9
|
import type {
|
|
11
10
|
EffectiveStatusLineSettings,
|
|
@@ -18,7 +17,8 @@ import type {
|
|
|
18
17
|
StatusLineSettings,
|
|
19
18
|
} from "./types.js";
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
/** Resolved per call: PI_CODING_AGENT_DIR may change after import. */
|
|
21
|
+
export const settingsPath = toppingSettingsPath("pi-topping-statusline");
|
|
22
22
|
|
|
23
23
|
export const SEPARATORS: readonly StatusLineSeparatorStyle[] = ["powerline", "powerline-thin", "slash", "pipe", "ascii"];
|
|
24
24
|
const SYMBOL_PRESETS: SymbolPreset[] = ["nerd", "unicode", "ascii"];
|
|
@@ -27,12 +27,10 @@ export const BORDER_STYLES: readonly BorderStyle[] = ["rounded", "heavy", "doubl
|
|
|
27
27
|
export const FEED_FORMATS: readonly FeedFormat[] = ["currency", "number", "text"];
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Nothing is subscribed by default: the list comes from the user's settings. The feeds
|
|
31
|
+
* segment renders nothing while the list is empty, so switching it on costs nothing.
|
|
32
32
|
*/
|
|
33
|
-
export const DEFAULT_FEEDS: readonly StatusLineFeed[] = [
|
|
34
|
-
{ customType: "pi-prompt-cache/savings", field: "savedUsd", prefix: "CS", format: "currency" },
|
|
35
|
-
];
|
|
33
|
+
export const DEFAULT_FEEDS: readonly StatusLineFeed[] = [];
|
|
36
34
|
|
|
37
35
|
export const DEFAULT_SEGMENTS: Required<StatusLineSegmentToggles> = {
|
|
38
36
|
pi: true,
|
|
@@ -49,6 +47,7 @@ export const DEFAULT_SEGMENTS: Required<StatusLineSegmentToggles> = {
|
|
|
49
47
|
feeds: false,
|
|
50
48
|
feedsBottomLeft: false,
|
|
51
49
|
tokenRateBottomLeft: false,
|
|
50
|
+
cacheHitBar: true,
|
|
52
51
|
contextBar: true,
|
|
53
52
|
contextStats: true,
|
|
54
53
|
scrollHint: true,
|
|
@@ -75,41 +74,48 @@ export function sanitizeFeeds(raw: unknown): StatusLineFeed[] {
|
|
|
75
74
|
return feeds;
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
function
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
settings.segments = segments;
|
|
77
|
+
function sanitizeSettings(parsed: unknown): StatusLineSettings {
|
|
78
|
+
if (!sanitize.isRecord(parsed)) return {};
|
|
79
|
+
const raw = parsed as StatusLineSettings;
|
|
80
|
+
const settings: StatusLineSettings = {};
|
|
81
|
+
if (typeof raw.transparent === "boolean") settings.transparent = raw.transparent;
|
|
82
|
+
if (raw.separator && SEPARATORS.includes(raw.separator)) settings.separator = raw.separator;
|
|
83
|
+
if (raw.symbols && SYMBOL_PRESETS.includes(raw.symbols)) settings.symbols = raw.symbols;
|
|
84
|
+
if (raw.borderStyle && BORDER_STYLES.includes(raw.borderStyle)) settings.borderStyle = raw.borderStyle;
|
|
85
|
+
if (raw.segments && typeof raw.segments === "object") {
|
|
86
|
+
const segments: StatusLineSegmentToggles = {};
|
|
87
|
+
for (const key of SEGMENT_KEYS) {
|
|
88
|
+
if (typeof raw.segments[key] === "boolean") segments[key] = raw.segments[key];
|
|
92
89
|
}
|
|
93
|
-
|
|
94
|
-
if (typeof raw.rainbowBorder === "boolean") settings.rainbowBorder = raw.rainbowBorder;
|
|
95
|
-
if (typeof raw.rainbowAnimation === "boolean") settings.rainbowAnimation = raw.rainbowAnimation;
|
|
96
|
-
if (typeof raw.nvidiaGreenBorder === "boolean") settings.nvidiaGreenBorder = raw.nvidiaGreenBorder;
|
|
97
|
-
if (typeof raw.nvidiaGreenAnimation === "boolean") settings.nvidiaGreenAnimation = raw.nvidiaGreenAnimation;
|
|
98
|
-
if (typeof raw.embedWorkingStatus === "boolean") settings.embedWorkingStatus = raw.embedWorkingStatus;
|
|
99
|
-
if (typeof raw.embedCompactionProgress === "boolean") settings.embedCompactionProgress = raw.embedCompactionProgress;
|
|
100
|
-
return settings;
|
|
101
|
-
} catch {
|
|
102
|
-
// Missing or corrupt settings.json falls back to defaults.
|
|
103
|
-
return {};
|
|
90
|
+
settings.segments = segments;
|
|
104
91
|
}
|
|
92
|
+
if (raw.feeds !== undefined) settings.feeds = sanitizeFeeds(raw.feeds);
|
|
93
|
+
if (typeof raw.rainbowBorder === "boolean") settings.rainbowBorder = raw.rainbowBorder;
|
|
94
|
+
if (typeof raw.rainbowAnimation === "boolean") settings.rainbowAnimation = raw.rainbowAnimation;
|
|
95
|
+
if (typeof raw.nvidiaGreenBorder === "boolean") settings.nvidiaGreenBorder = raw.nvidiaGreenBorder;
|
|
96
|
+
if (typeof raw.nvidiaGreenAnimation === "boolean") settings.nvidiaGreenAnimation = raw.nvidiaGreenAnimation;
|
|
97
|
+
if (typeof raw.embedWorkingStatus === "boolean") settings.embedWorkingStatus = raw.embedWorkingStatus;
|
|
98
|
+
if (typeof raw.embedCompactionProgress === "boolean") settings.embedCompactionProgress = raw.embedCompactionProgress;
|
|
99
|
+
if (typeof raw.cacheHitBarSegments === "number") settings.cacheHitBarSegments = clampCacheHitCells(raw.cacheHitBarSegments);
|
|
100
|
+
return settings;
|
|
105
101
|
}
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
103
|
+
const settingsStore = createSettingsStore<StatusLineSettings>({
|
|
104
|
+
path: settingsPath,
|
|
105
|
+
codec: {
|
|
106
|
+
sanitize: sanitizeSettings,
|
|
107
|
+
serialize: (settings) => `${JSON.stringify(settings, null, "\t")}\n`,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/** Missing or corrupt settings.json falls back to defaults. */
|
|
112
|
+
export function loadSettings(): StatusLineSettings {
|
|
113
|
+
return settingsStore.read();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Atomic write; the result says whether it landed so callers can surface a failure. */
|
|
117
|
+
export function saveSettings(settings: StatusLineSettings): SettingsWriteResult<StatusLineSettings> {
|
|
118
|
+
return settingsStore.write(settings);
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
export function resolveEffectiveSettings(settings: StatusLineSettings): EffectiveStatusLineSettings {
|
|
@@ -126,6 +132,7 @@ export function resolveEffectiveSettings(settings: StatusLineSettings): Effectiv
|
|
|
126
132
|
if (seg.feeds) bottomRightSegments.push("feeds");
|
|
127
133
|
if (seg.tokenRate) bottomRightSegments.push("token_rate");
|
|
128
134
|
if (seg.piStats) bottomRightSegments.push("pi_stats");
|
|
135
|
+
if (seg.cacheHitBar) bottomRightSegments.push("cache_hit_graph");
|
|
129
136
|
if (seg.contextBar || seg.contextStats) bottomRightSegments.push("context_graph");
|
|
130
137
|
|
|
131
138
|
const rightSegments: StatusLineSegmentId[] = [];
|
|
@@ -145,6 +152,7 @@ export function resolveEffectiveSettings(settings: StatusLineSettings): Effectiv
|
|
|
145
152
|
pr: allSegments.includes("pr"),
|
|
146
153
|
piStats: allSegments.includes("pi_stats"),
|
|
147
154
|
tokenRate: allSegments.includes("token_rate"),
|
|
155
|
+
cacheHit: allSegments.includes("cache_hit_graph"),
|
|
148
156
|
feeds: allSegments.includes("feeds") ? feeds.map(feed => feed.customType) : [],
|
|
149
157
|
};
|
|
150
158
|
|
|
@@ -168,6 +176,7 @@ export function resolveEffectiveSettings(settings: StatusLineSettings): Effectiv
|
|
|
168
176
|
model: { showModel: seg.model, showProvider: seg.provider, showThinking: seg.thinking },
|
|
169
177
|
path: { abbreviate: true, maxLength: 40, stripWorkPrefix: true },
|
|
170
178
|
context: { showBar: seg.contextBar, showStats: seg.contextStats },
|
|
179
|
+
cacheHit: { cells: clampCacheHitCells(settings.cacheHitBarSegments) },
|
|
171
180
|
feeds,
|
|
172
181
|
},
|
|
173
182
|
};
|