@eiei114/pi-sub-bar 1.5.1
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 +201 -0
- package/README.md +200 -0
- package/index.ts +1103 -0
- package/package.json +39 -0
- package/src/core-settings.ts +25 -0
- package/src/dividers.ts +48 -0
- package/src/errors.ts +71 -0
- package/src/formatting.ts +937 -0
- package/src/paths.ts +21 -0
- package/src/providers/extras.ts +21 -0
- package/src/providers/metadata.ts +199 -0
- package/src/providers/settings.ts +359 -0
- package/src/providers/windows.ts +23 -0
- package/src/settings/display.ts +786 -0
- package/src/settings/menu.ts +183 -0
- package/src/settings/themes.ts +378 -0
- package/src/settings/ui.ts +1388 -0
- package/src/settings-types.ts +651 -0
- package/src/settings-ui.ts +5 -0
- package/src/settings.ts +176 -0
- package/src/share.ts +75 -0
- package/src/status.ts +103 -0
- package/src/storage.ts +61 -0
- package/src/types.ts +25 -0
- package/src/ui/keybindings.ts +92 -0
- package/src/ui/settings-list.ts +304 -0
- package/src/usage/types.ts +5 -0
- package/src/utils.ts +42 -0
- package/test/all.test.ts +6 -0
- package/test/dividers.test.ts +34 -0
- package/test/formatting.test.ts +437 -0
- package/test/keybindings.test.ts +59 -0
- package/test/providers.test.ts +42 -0
- package/test/settings.test.ts +336 -0
- package/test/status.test.ts +27 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { visibleWidth } from "@mariozechner/pi-tui";
|
|
4
|
+
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
5
|
+
import { formatUsageStatus, formatUsageStatusWithWidth, formatUsageWindowParts } from "../src/formatting.js";
|
|
6
|
+
import { shouldShowWindow } from "../src/providers/windows.js";
|
|
7
|
+
import { getDefaultSettings } from "../src/settings-types.js";
|
|
8
|
+
import type { UsageSnapshot } from "../src/types.js";
|
|
9
|
+
|
|
10
|
+
const theme = {
|
|
11
|
+
fg: (_color: string, text: string) => text,
|
|
12
|
+
bold: (text: string) => text,
|
|
13
|
+
} as unknown as Theme;
|
|
14
|
+
|
|
15
|
+
function buildUsage(): UsageSnapshot {
|
|
16
|
+
return {
|
|
17
|
+
provider: "codex",
|
|
18
|
+
displayName: "Codex Plan",
|
|
19
|
+
windows: [
|
|
20
|
+
{
|
|
21
|
+
label: "5h",
|
|
22
|
+
usedPercent: 3,
|
|
23
|
+
resetDescription: "4h",
|
|
24
|
+
resetAt: new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString(),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "Week",
|
|
28
|
+
usedPercent: 7,
|
|
29
|
+
resetDescription: "6d",
|
|
30
|
+
resetAt: new Date(Date.now() + 6 * 24 * 60 * 60 * 1000).toISOString(),
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildUsageWithStatus(
|
|
37
|
+
indicator: NonNullable<UsageSnapshot["status"]>["indicator"],
|
|
38
|
+
description?: string
|
|
39
|
+
): UsageSnapshot {
|
|
40
|
+
return {
|
|
41
|
+
...buildUsage(),
|
|
42
|
+
status: {
|
|
43
|
+
indicator,
|
|
44
|
+
description,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("fill width with contained vertical bars does not overflow", () => {
|
|
50
|
+
const settings = getDefaultSettings();
|
|
51
|
+
settings.display.barType = "vertical";
|
|
52
|
+
settings.display.barWidth = "fill";
|
|
53
|
+
settings.display.containBar = true;
|
|
54
|
+
|
|
55
|
+
const output = formatUsageStatusWithWidth(theme, buildUsage(), 80, undefined, settings);
|
|
56
|
+
assert.ok(output);
|
|
57
|
+
assert.ok(visibleWidth(output) <= 80);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("fill width with contained horizontal bars does not overflow", () => {
|
|
61
|
+
const settings = getDefaultSettings();
|
|
62
|
+
settings.display.barType = "horizontal-bar";
|
|
63
|
+
settings.display.barWidth = "fill";
|
|
64
|
+
settings.display.containBar = true;
|
|
65
|
+
|
|
66
|
+
const output = formatUsageStatusWithWidth(theme, buildUsage(), 80, undefined, settings);
|
|
67
|
+
assert.ok(output);
|
|
68
|
+
assert.ok(visibleWidth(output) <= 80);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("bar width 1 contained vertical bars stay compact", () => {
|
|
72
|
+
const settings = getDefaultSettings();
|
|
73
|
+
settings.display.barType = "vertical";
|
|
74
|
+
settings.display.barWidth = 1;
|
|
75
|
+
settings.display.containBar = true;
|
|
76
|
+
|
|
77
|
+
const output = formatUsageStatus(theme, buildUsage(), undefined, settings);
|
|
78
|
+
assert.ok(output);
|
|
79
|
+
assert.match(output, /▕▁▏/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("status indicator layout includes icon text provider colon", () => {
|
|
83
|
+
const settings = getDefaultSettings();
|
|
84
|
+
settings.display.statusIndicatorMode = "icon+text";
|
|
85
|
+
settings.display.statusIconPack = "minimal";
|
|
86
|
+
settings.display.statusDismissOk = false;
|
|
87
|
+
settings.display.providerLabelColon = true;
|
|
88
|
+
|
|
89
|
+
const output = formatUsageStatus(
|
|
90
|
+
theme,
|
|
91
|
+
buildUsageWithStatus("major", "Outage"),
|
|
92
|
+
undefined,
|
|
93
|
+
settings,
|
|
94
|
+
);
|
|
95
|
+
assert.ok(output);
|
|
96
|
+
assert.ok(output.startsWith("⚠ Outage Codex:"));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("status/provider divider renders only when status is present", () => {
|
|
100
|
+
const settings = getDefaultSettings();
|
|
101
|
+
settings.display.statusIndicatorMode = "icon+text";
|
|
102
|
+
settings.display.statusIconPack = "minimal";
|
|
103
|
+
settings.display.statusDismissOk = false;
|
|
104
|
+
settings.display.statusProviderDivider = true;
|
|
105
|
+
settings.display.dividerCharacter = "│";
|
|
106
|
+
|
|
107
|
+
const output = formatUsageStatus(
|
|
108
|
+
theme,
|
|
109
|
+
buildUsageWithStatus("major", "Outage"),
|
|
110
|
+
undefined,
|
|
111
|
+
settings,
|
|
112
|
+
);
|
|
113
|
+
assert.ok(output.includes("│"));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("custom status icon pack uses provided characters", () => {
|
|
117
|
+
const settings = getDefaultSettings();
|
|
118
|
+
settings.display.statusIndicatorMode = "icon";
|
|
119
|
+
settings.display.statusIconPack = "custom";
|
|
120
|
+
settings.display.statusIconCustom = "o!x?";
|
|
121
|
+
settings.display.statusDismissOk = false;
|
|
122
|
+
|
|
123
|
+
const output = formatUsageStatus(
|
|
124
|
+
theme,
|
|
125
|
+
buildUsageWithStatus("major"),
|
|
126
|
+
undefined,
|
|
127
|
+
settings,
|
|
128
|
+
);
|
|
129
|
+
assert.ok(output.includes("x"));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("status dismiss ok hides operational text", () => {
|
|
133
|
+
const settings = getDefaultSettings();
|
|
134
|
+
settings.display.statusIndicatorMode = "icon+text";
|
|
135
|
+
settings.display.statusIconPack = "emoji";
|
|
136
|
+
settings.display.statusDismissOk = true;
|
|
137
|
+
|
|
138
|
+
const output = formatUsageStatus(
|
|
139
|
+
theme,
|
|
140
|
+
buildUsageWithStatus("none", "All Systems Operational"),
|
|
141
|
+
undefined,
|
|
142
|
+
settings,
|
|
143
|
+
);
|
|
144
|
+
assert.ok(output);
|
|
145
|
+
assert.ok(!output.includes("Operational"));
|
|
146
|
+
assert.ok(!output.includes("✅"));
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("unknown status shows label in text mode", () => {
|
|
150
|
+
const settings = getDefaultSettings();
|
|
151
|
+
settings.display.statusIndicatorMode = "text";
|
|
152
|
+
settings.display.statusIconPack = "emoji";
|
|
153
|
+
settings.display.statusDismissOk = false;
|
|
154
|
+
|
|
155
|
+
const output = formatUsageStatus(
|
|
156
|
+
theme,
|
|
157
|
+
buildUsageWithStatus("unknown"),
|
|
158
|
+
undefined,
|
|
159
|
+
settings,
|
|
160
|
+
);
|
|
161
|
+
assert.ok(!output.includes("❓"));
|
|
162
|
+
assert.ok(output.includes("Status Unknown"));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("fetch errors rely on status text instead of appended warning", () => {
|
|
166
|
+
const settings = getDefaultSettings();
|
|
167
|
+
settings.display.statusIndicatorMode = "text";
|
|
168
|
+
|
|
169
|
+
const usage = buildUsage();
|
|
170
|
+
usage.error = { code: "FETCH_FAILED", message: "Fetch failed" };
|
|
171
|
+
usage.lastSuccessAt = Date.now() - 5 * 60 * 1000;
|
|
172
|
+
usage.status = { indicator: "minor", description: "Fetch failed" };
|
|
173
|
+
|
|
174
|
+
const output = formatUsageStatus(theme, usage, undefined, settings);
|
|
175
|
+
assert.ok(output);
|
|
176
|
+
assert.ok(output.includes("Last upd.: 5m ago"));
|
|
177
|
+
assert.ok(!output.includes("(Fetch failed)"));
|
|
178
|
+
assert.ok(output.includes("5h"));
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("background base text color uses theme background ansi", () => {
|
|
182
|
+
const settings = getDefaultSettings();
|
|
183
|
+
settings.display.baseTextColor = "selectedBg";
|
|
184
|
+
|
|
185
|
+
const bgTheme = {
|
|
186
|
+
fg: (_color: string, text: string) => text,
|
|
187
|
+
bold: (text: string) => text,
|
|
188
|
+
getBgAnsi: (_color: string) => "\x1b[48;5;120m",
|
|
189
|
+
} as unknown as Theme;
|
|
190
|
+
|
|
191
|
+
const output = formatUsageStatus(bgTheme, buildUsage(), undefined, settings);
|
|
192
|
+
assert.ok(output);
|
|
193
|
+
assert.ok(output.includes("\x1b[38;5;120m"));
|
|
194
|
+
assert.ok(output.includes("\x1b[39m"));
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("reset timer placement works without titles or usage labels", () => {
|
|
198
|
+
const settings = getDefaultSettings();
|
|
199
|
+
settings.display.showWindowTitle = false;
|
|
200
|
+
settings.display.showUsageLabels = false;
|
|
201
|
+
settings.display.barStyle = "percentage";
|
|
202
|
+
|
|
203
|
+
const usage = buildUsage();
|
|
204
|
+
const window = usage.windows[0];
|
|
205
|
+
|
|
206
|
+
const cases: Array<{ position: "off" | "front" | "back" | "integrated"; label: boolean; reset: boolean }> = [
|
|
207
|
+
{ position: "front", label: true, reset: false },
|
|
208
|
+
{ position: "back", label: false, reset: true },
|
|
209
|
+
{ position: "integrated", label: true, reset: false },
|
|
210
|
+
{ position: "off", label: false, reset: false },
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
for (const entry of cases) {
|
|
214
|
+
settings.display.resetTimePosition = entry.position;
|
|
215
|
+
const parts = formatUsageWindowParts(theme, window, false, settings, usage);
|
|
216
|
+
assert.equal(parts.label.includes("5h"), false);
|
|
217
|
+
assert.equal(parts.pct.includes("used"), false);
|
|
218
|
+
if (entry.label) {
|
|
219
|
+
assert.ok(parts.label.includes("4h"));
|
|
220
|
+
} else {
|
|
221
|
+
assert.equal(parts.label, "");
|
|
222
|
+
}
|
|
223
|
+
if (entry.reset) {
|
|
224
|
+
assert.ok(parts.reset.includes("4h"));
|
|
225
|
+
} else {
|
|
226
|
+
assert.equal(parts.reset, "");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("extras render even when usage windows are hidden", () => {
|
|
232
|
+
const settings = getDefaultSettings();
|
|
233
|
+
settings.providers.anthropic.windows.show5h = false;
|
|
234
|
+
settings.providers.anthropic.windows.show7d = false;
|
|
235
|
+
settings.providers.anthropic.windows.showExtra = true;
|
|
236
|
+
|
|
237
|
+
const usage: UsageSnapshot = {
|
|
238
|
+
provider: "anthropic",
|
|
239
|
+
displayName: "Anthropic (Claude)",
|
|
240
|
+
windows: [
|
|
241
|
+
{ label: "5h", usedPercent: 10 },
|
|
242
|
+
{ label: "Week", usedPercent: 20 },
|
|
243
|
+
],
|
|
244
|
+
extraUsageEnabled: false,
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const output = formatUsageStatus(theme, usage, undefined, settings);
|
|
248
|
+
assert.ok(output);
|
|
249
|
+
assert.ok(output.includes("Extra [off]"));
|
|
250
|
+
assert.ok(!output.includes("5h"));
|
|
251
|
+
assert.ok(!output.includes("Week"));
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("percentage labels clamp to bounds", () => {
|
|
255
|
+
const settings = getDefaultSettings();
|
|
256
|
+
settings.display.barStyle = "percentage";
|
|
257
|
+
settings.display.showUsageLabels = false;
|
|
258
|
+
|
|
259
|
+
const usage = buildUsage();
|
|
260
|
+
const highWindow = { ...usage.windows[0], usedPercent: 150 };
|
|
261
|
+
const highParts = formatUsageWindowParts(theme, highWindow, false, settings, usage);
|
|
262
|
+
assert.ok(highParts.pct.includes("100%"));
|
|
263
|
+
|
|
264
|
+
const lowWindow = { ...usage.windows[0], usedPercent: -20 };
|
|
265
|
+
const lowParts = formatUsageWindowParts(theme, lowWindow, false, settings, usage);
|
|
266
|
+
assert.ok(lowParts.pct.includes("0%"));
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("emoji bar characters respect narrow widths", () => {
|
|
270
|
+
const settings = getDefaultSettings();
|
|
271
|
+
settings.display.barType = "horizontal-bar";
|
|
272
|
+
settings.display.barStyle = "bar";
|
|
273
|
+
settings.display.barWidth = 1;
|
|
274
|
+
settings.display.barCharacter = "🚀";
|
|
275
|
+
|
|
276
|
+
const usage = buildUsage();
|
|
277
|
+
const parts = formatUsageWindowParts(theme, usage.windows[0], false, settings, usage);
|
|
278
|
+
assert.equal(visibleWidth(parts.bar), 1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("percentage style ignores containBar caps", () => {
|
|
282
|
+
const settings = getDefaultSettings();
|
|
283
|
+
settings.display.barStyle = "percentage";
|
|
284
|
+
settings.display.containBar = true;
|
|
285
|
+
|
|
286
|
+
const usage = buildUsage();
|
|
287
|
+
const parts = formatUsageWindowParts(theme, usage.windows[0], false, settings, usage);
|
|
288
|
+
assert.equal(parts.bar, "");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("divider fill and label gap fill stay within width", () => {
|
|
292
|
+
const settings = getDefaultSettings();
|
|
293
|
+
settings.display.dividerBlanks = "fill";
|
|
294
|
+
settings.display.barWidth = 6;
|
|
295
|
+
|
|
296
|
+
const output = formatUsageStatusWithWidth(theme, buildUsage(), 60, undefined, settings, { labelGapFill: true });
|
|
297
|
+
assert.ok(output);
|
|
298
|
+
assert.ok(visibleWidth(output) <= 60);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("narrow widths truncate without errors", () => {
|
|
302
|
+
const settings = getDefaultSettings();
|
|
303
|
+
settings.display.barWidth = 6;
|
|
304
|
+
|
|
305
|
+
const output = formatUsageStatusWithWidth(theme, buildUsage(), 10, undefined, settings, { labelGapFill: true });
|
|
306
|
+
assert.ok(output);
|
|
307
|
+
assert.ok(visibleWidth(output) <= 10);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("fill bars with extras stay within width", () => {
|
|
311
|
+
const settings = getDefaultSettings();
|
|
312
|
+
settings.display.barWidth = "fill";
|
|
313
|
+
settings.display.containBar = true;
|
|
314
|
+
settings.display.dividerBlanks = "fill";
|
|
315
|
+
settings.providers.copilot.showMultiplier = true;
|
|
316
|
+
settings.providers.copilot.showRequestsLeft = true;
|
|
317
|
+
|
|
318
|
+
const usage: UsageSnapshot = {
|
|
319
|
+
provider: "copilot",
|
|
320
|
+
displayName: "GitHub Copilot",
|
|
321
|
+
windows: [{ label: "Month", usedPercent: 12 }],
|
|
322
|
+
requestsRemaining: 120,
|
|
323
|
+
requestsEntitlement: 200,
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const output = formatUsageStatusWithWidth(theme, usage, 140, "GPT-4o", settings, { labelGapFill: true });
|
|
327
|
+
assert.ok(output);
|
|
328
|
+
assert.ok(output.includes("Model multiplier"));
|
|
329
|
+
assert.ok(visibleWidth(output) <= 140);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
test("codex shows model-specific usage for GPT-5.3-Codex-Spark", () => {
|
|
334
|
+
const settings = getDefaultSettings();
|
|
335
|
+
const usage: UsageSnapshot = {
|
|
336
|
+
provider: "codex",
|
|
337
|
+
displayName: "Codex Plan",
|
|
338
|
+
windows: [
|
|
339
|
+
{ label: "5h", usedPercent: 12 },
|
|
340
|
+
{ label: "GPT-5.3-Codex-Spark 5h", usedPercent: 3 },
|
|
341
|
+
{ label: "GPT-5.3-Codex-Spark Week", usedPercent: 4 },
|
|
342
|
+
],
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
assert.equal(shouldShowWindow(usage, usage.windows[1], settings, { id: "gpt-5.3-codex-spark" }), true);
|
|
346
|
+
assert.equal(shouldShowWindow(usage, usage.windows[0], settings, { id: "gpt-5.3-codex-spark" }), false);
|
|
347
|
+
|
|
348
|
+
assert.equal(shouldShowWindow(usage, usage.windows[1], settings, { id: "gpt-4o" }), false);
|
|
349
|
+
assert.equal(shouldShowWindow(usage, usage.windows[0], settings, { id: "gpt-4o" }), true);
|
|
350
|
+
assert.equal(shouldShowWindow(usage, usage.windows[2], settings, { id: "gpt-4o" }), false);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("codex spark usage window labels hide model prefix", () => {
|
|
354
|
+
const settings = getDefaultSettings();
|
|
355
|
+
const usage: UsageSnapshot = {
|
|
356
|
+
provider: "codex",
|
|
357
|
+
displayName: "Codex Plan",
|
|
358
|
+
windows: [
|
|
359
|
+
{ label: "GPT-5.3-Codex-Spark 5h", usedPercent: 3 },
|
|
360
|
+
{ label: "GPT-5.3-Codex-Spark Week", usedPercent: 4 },
|
|
361
|
+
],
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const output = formatUsageStatus(theme, usage, "gpt-5.3-codex-spark", settings);
|
|
365
|
+
assert.equal(output?.includes("GPT-5.3-Codex-Spark"), false);
|
|
366
|
+
assert.equal(output?.includes("5h"), true);
|
|
367
|
+
assert.equal(output?.includes("Week"), true);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("codex spark provider label uses Codex (Spark)", () => {
|
|
371
|
+
const settings = getDefaultSettings();
|
|
372
|
+
const usage: UsageSnapshot = {
|
|
373
|
+
provider: "codex",
|
|
374
|
+
displayName: "Codex Plan",
|
|
375
|
+
windows: [{ label: "5h", usedPercent: 3 }],
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
const output = formatUsageStatus(theme, usage, "gpt-5.3-codex-spark", settings);
|
|
379
|
+
assert.equal(output?.includes("Codex (Spark)"), true);
|
|
380
|
+
assert.equal(output?.includes("Codex Plan"), false);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("context bar appears as leftmost element when enabled", () => {
|
|
384
|
+
const settings = getDefaultSettings();
|
|
385
|
+
settings.display.showContextBar = true;
|
|
386
|
+
settings.display.barWidth = 6;
|
|
387
|
+
|
|
388
|
+
const contextInfo = { tokens: 50000, contextWindow: 200000, percent: 25 };
|
|
389
|
+
const output = formatUsageStatus(theme, buildUsage(), undefined, settings, contextInfo);
|
|
390
|
+
assert.ok(output);
|
|
391
|
+
assert.ok(output.includes("Ctx"));
|
|
392
|
+
assert.ok(output.includes("25%"));
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("context bar is hidden when showContextBar is false", () => {
|
|
396
|
+
const settings = getDefaultSettings();
|
|
397
|
+
settings.display.showContextBar = false;
|
|
398
|
+
settings.display.barWidth = 6;
|
|
399
|
+
|
|
400
|
+
const contextInfo = { tokens: 50000, contextWindow: 200000, percent: 25 };
|
|
401
|
+
const output = formatUsageStatus(theme, buildUsage(), undefined, settings, contextInfo);
|
|
402
|
+
assert.ok(output);
|
|
403
|
+
assert.ok(!output.includes("Ctx"));
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
test("context bar with fill width stays within bounds", () => {
|
|
407
|
+
const settings = getDefaultSettings();
|
|
408
|
+
settings.display.showContextBar = true;
|
|
409
|
+
settings.display.barWidth = "fill";
|
|
410
|
+
settings.display.containBar = true;
|
|
411
|
+
|
|
412
|
+
const contextInfo = { tokens: 100000, contextWindow: 200000, percent: 50 };
|
|
413
|
+
const output = formatUsageStatusWithWidth(
|
|
414
|
+
theme,
|
|
415
|
+
buildUsage(),
|
|
416
|
+
100,
|
|
417
|
+
undefined,
|
|
418
|
+
settings,
|
|
419
|
+
{ labelGapFill: true },
|
|
420
|
+
contextInfo
|
|
421
|
+
);
|
|
422
|
+
assert.ok(output);
|
|
423
|
+
assert.ok(output.includes("Ctx"));
|
|
424
|
+
assert.ok(visibleWidth(output) <= 100);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("context bar not shown when contextWindow is 0", () => {
|
|
428
|
+
const settings = getDefaultSettings();
|
|
429
|
+
settings.display.showContextBar = true;
|
|
430
|
+
settings.display.barWidth = 6;
|
|
431
|
+
|
|
432
|
+
const contextInfo = { tokens: 0, contextWindow: 0, percent: 0 };
|
|
433
|
+
const output = formatUsageStatus(theme, buildUsage(), undefined, settings, contextInfo);
|
|
434
|
+
assert.ok(output);
|
|
435
|
+
assert.ok(!output.includes("Ctx"));
|
|
436
|
+
|
|
437
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { createSettingsKeybindings } from "../src/ui/keybindings.js";
|
|
4
|
+
|
|
5
|
+
test("settings keybindings prefer editor keybindings when available", () => {
|
|
6
|
+
const kb = createSettingsKeybindings({
|
|
7
|
+
getEditorKeybindings: () => ({
|
|
8
|
+
matches: (data, action) => data === "X" && action === "selectDown",
|
|
9
|
+
}),
|
|
10
|
+
getKeybindings: () => ({
|
|
11
|
+
matches: () => {
|
|
12
|
+
throw new Error("legacy keybindings should not be used when editor keybindings exist");
|
|
13
|
+
},
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
assert.equal(kb.matches("X", "selectDown"), true);
|
|
18
|
+
assert.equal(kb.matches("X", "selectUp"), false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("settings keybindings map actions to legacy keybinding IDs", () => {
|
|
22
|
+
const seen: string[] = [];
|
|
23
|
+
const kb = createSettingsKeybindings({
|
|
24
|
+
getKeybindings: () => ({
|
|
25
|
+
matches: (_data, action) => {
|
|
26
|
+
seen.push(action);
|
|
27
|
+
return action === "tui.select.down";
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert.equal(kb.matches("ignored", "selectDown"), true);
|
|
33
|
+
assert.equal(kb.matches("ignored", "cursorLeft"), false);
|
|
34
|
+
assert.deepEqual(seen, ["tui.select.down", "tui.editor.cursorLeft"]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("settings keybindings fallback uses matchesKey helper when no keybinding manager exists", () => {
|
|
38
|
+
const kb = createSettingsKeybindings({
|
|
39
|
+
matchesKey: (data, key) => data === `<<${key}>>`,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
assert.equal(kb.matches("<<up>>", "selectUp"), true);
|
|
43
|
+
assert.equal(kb.matches("<<down>>", "selectDown"), true);
|
|
44
|
+
assert.equal(kb.matches("<<left>>", "cursorLeft"), true);
|
|
45
|
+
assert.equal(kb.matches("<<enter>>", "selectConfirm"), true);
|
|
46
|
+
assert.equal(kb.matches("<<escape>>", "selectCancel"), true);
|
|
47
|
+
assert.equal(kb.matches("<<right>>", "cursorLeft"), false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("settings keybindings fallback handles raw escape sequences", () => {
|
|
51
|
+
const kb = createSettingsKeybindings({});
|
|
52
|
+
|
|
53
|
+
assert.equal(kb.matches("\u001b[A", "selectUp"), true);
|
|
54
|
+
assert.equal(kb.matches("\u001b[B", "selectDown"), true);
|
|
55
|
+
assert.equal(kb.matches("\u001b[D", "cursorLeft"), true);
|
|
56
|
+
assert.equal(kb.matches("\u001b[C", "cursorRight"), true);
|
|
57
|
+
assert.equal(kb.matches("\r", "selectConfirm"), true);
|
|
58
|
+
assert.equal(kb.matches("\u001b", "selectCancel"), true);
|
|
59
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { getUsageExtras } from "../src/providers/extras.js";
|
|
4
|
+
import { getDefaultSettings } from "../src/settings-types.js";
|
|
5
|
+
import type { UsageSnapshot } from "../src/types.js";
|
|
6
|
+
|
|
7
|
+
function buildCopilotUsage(): UsageSnapshot {
|
|
8
|
+
return {
|
|
9
|
+
provider: "copilot",
|
|
10
|
+
displayName: "GitHub Copilot",
|
|
11
|
+
windows: [],
|
|
12
|
+
requestsRemaining: 100,
|
|
13
|
+
requestsEntitlement: 200,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test("copilot extras include multiplier and requests left", () => {
|
|
18
|
+
const settings = getDefaultSettings();
|
|
19
|
+
settings.providers.copilot.showMultiplier = true;
|
|
20
|
+
settings.providers.copilot.showRequestsLeft = true;
|
|
21
|
+
|
|
22
|
+
const extras = getUsageExtras(buildCopilotUsage(), settings, "GPT-4o");
|
|
23
|
+
assert.equal(extras.length, 1);
|
|
24
|
+
assert.ok(extras[0].label.includes("Model multiplier: 0x"));
|
|
25
|
+
assert.ok(extras[0].label.includes("req. left"));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("copilot extras respect toggle settings", () => {
|
|
29
|
+
const settings = getDefaultSettings();
|
|
30
|
+
settings.providers.copilot.showMultiplier = false;
|
|
31
|
+
|
|
32
|
+
const extras = getUsageExtras(buildCopilotUsage(), settings, "GPT-4o");
|
|
33
|
+
assert.equal(extras.length, 0);
|
|
34
|
+
|
|
35
|
+
settings.providers.copilot.showMultiplier = true;
|
|
36
|
+
settings.providers.copilot.showRequestsLeft = false;
|
|
37
|
+
|
|
38
|
+
const withMultiplierOnly = getUsageExtras(buildCopilotUsage(), settings, "GPT-4o");
|
|
39
|
+
assert.equal(withMultiplierOnly.length, 1);
|
|
40
|
+
assert.ok(withMultiplierOnly[0].label.includes("Model multiplier: 0x"));
|
|
41
|
+
assert.ok(!withMultiplierOnly[0].label.includes("req. left"));
|
|
42
|
+
});
|