osdy-pi 1.2.0 → 1.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 +160 -30
- package/bin/osdy-pi.mjs +29 -6
- package/bin/osdy.mjs +43 -0
- package/extensions/osdy-pi/account-profiles.test.ts +111 -97
- package/extensions/osdy-pi/account-profiles.ts +91 -162
- package/extensions/osdy-pi/animation.test.ts +84 -0
- package/extensions/osdy-pi/animation.ts +17 -8
- package/extensions/osdy-pi/codex-usage-ui.test.ts +1145 -0
- package/extensions/osdy-pi/codex-usage-ui.ts +728 -0
- package/extensions/osdy-pi/codex-usage.test.ts +276 -0
- package/extensions/osdy-pi/codex-usage.ts +276 -0
- package/extensions/osdy-pi/constants.ts +134 -34
- package/extensions/osdy-pi/context-usage-ui.test.ts +168 -0
- package/extensions/osdy-pi/context-usage-ui.ts +60 -0
- package/extensions/osdy-pi/editor-settings.test.ts +121 -23
- package/extensions/osdy-pi/editor-settings.ts +39 -3
- package/extensions/osdy-pi/metrics.test.ts +95 -0
- package/extensions/osdy-pi/metrics.ts +39 -1
- package/extensions/osdy-pi/profile-label.ts +2 -2
- package/extensions/osdy-pi/runtime-helpers.test.ts +220 -1
- package/extensions/osdy-pi/runtime-helpers.ts +42 -19
- package/extensions/osdy-pi/runtime.test.ts +355 -7
- package/extensions/osdy-pi/runtime.ts +344 -66
- package/extensions/osdy-pi/types.ts +26 -1
- package/extensions/osdy-pi/ui.test.ts +128 -0
- package/extensions/osdy-pi/ui.ts +57 -40
- package/package.json +5 -2
- package/scripts/osdy-pi-account-profiles.mjs +521 -76
- package/scripts/osdy-pi-gentle-coexistence.mjs +200 -0
- package/scripts/osdy-pi-profile-setup.mjs +175 -0
- package/themes/osdy-pi-catppuccin-frappe.json +6 -2
- package/themes/osdy-pi-catppuccin-latte.json +6 -2
- package/themes/osdy-pi-catppuccin-macchiato.json +6 -2
- package/themes/osdy-pi-catppuccin-mocha.json +6 -2
- package/themes/osdy-pi-dark.json +6 -2
- package/themes/osdy-pi-dracula.json +6 -2
- package/themes/osdy-pi-kanagawa-dragon.json +6 -2
- package/themes/osdy-pi-kanagawa-lotus.json +6 -2
- package/themes/osdy-pi-kanagawa-wave.json +6 -2
- package/themes/osdy-pi-lucent-orange.json +6 -2
- package/themes/osdy-pi-matrix.json +6 -2
- package/themes/osdy-pi-new.json +6 -2
- package/themes/osdy-pi-sexy.json +6 -2
- package/themes/osdy-pi-tokyo-night.json +6 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import type { HeaderVariant } from "./types.js";
|
|
2
|
+
import type { HeaderVariant, MascotChoice } from "./types.js";
|
|
3
3
|
|
|
4
4
|
export const ANIMATION_ENABLED = true;
|
|
5
5
|
export const ANIMATION_INTERVAL_MS = 30;
|
|
@@ -100,18 +100,62 @@ const HTML_MASCOT_MAP = [
|
|
|
100
100
|
" dmmmmmm ",
|
|
101
101
|
] as const;
|
|
102
102
|
|
|
103
|
-
const
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
"
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
103
|
+
const BTS_MASCOT = [
|
|
104
|
+
" ▒▒▒▒ ▒▒▒░ ",
|
|
105
|
+
" ▓▓▓▓▓▓▒ ░▒▓▓▓▓▓▒ ",
|
|
106
|
+
" ▓▓▓ ▒▓▓▒░ ▒▓▓▓░░▓▓▓ ",
|
|
107
|
+
" ░▓▓▒ ▒▒▓▒░░ ░▒▒▒▒▒░░ ▒▒▒▓▒ ▒▓ ",
|
|
108
|
+
" ░▓▓▒ ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ ▒▓ ",
|
|
109
|
+
" ▓▓▓ ░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░ ░▓▓▒ ",
|
|
110
|
+
" ░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░ ",
|
|
111
|
+
" ░▒▒▒▒▒▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒ ",
|
|
112
|
+
" ░▒▒▒▒▓▓▓▒ ▓▓▓▓▓▒▒▒▓▓▓▓▓ ░▓▓▓▓▒ ",
|
|
113
|
+
" ░▒▒▓▓▓▓ ░▓▓▓▒▒▒▓▓▓░ ▒▓▓▓░ ░ ▒▒ ",
|
|
114
|
+
" ▒▓▓▓▓░ ▓▓▒▒▒▓▓ ░▓▒░ ▓▓▓▓▓▓░ ",
|
|
115
|
+
" ▓▓▓▓▓▒ ▓▓ ▒▒▒░ ▓▓ ▓▓▓▓▓▓▓▓▓▓▓▓ ",
|
|
116
|
+
" ▒▓▓▓▒░ ░▓▓░░░▓▓░ ░▓▓▓▓▒▒▓▒▒▓▓▓▓",
|
|
117
|
+
" ░▓░ ▓▓▓▓ ▒▓▓▓▓ ▓▓▓▓▓▒▒▓▒▓▓▓▓░",
|
|
118
|
+
" ▒▓▒ ▓▓▓▓▓▓ ▒▓▓▓▓▓░ ░▓ ░▓▓▓▓▓▓▒▓▓▓ ",
|
|
119
|
+
" ▓▓░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▒░▒▓░ ░░▒▓▓▓░ ",
|
|
120
|
+
" ░▒▓▓▓▓▓▓▓▓▓▓▓▓▓▒ ",
|
|
121
|
+
" ▒▒▒▒▒░ ▒▒▒▒▒▒▒▓▓▓▓▓▓▒▒▒▒▒░ ░ ",
|
|
122
|
+
" ░░▒▒▒▒░░░░ ░░▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒░ ░░░ ░ ",
|
|
123
|
+
"▒▒▒▒▒▒ ░ ░▒▒▒░ ▒▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒░ ░ ",
|
|
124
|
+
"░▒▒░░ ░▒▒▒▒ ░▒▒▒▒░ ▓▓▓▓▓▓▓▓▓▓▓▒░ ░▒▒░░ ",
|
|
125
|
+
" ▒░ ░▒▒▒▒▒░ ░▒░ ▓▓▓▓▓▓▓▓▓▓▓▒▒▒ ",
|
|
126
|
+
" ▒▒▒▒░ ▒▒░ ░▒▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒ ",
|
|
127
|
+
" ▒▒▒▒░░ ▒▒▒▒▒▒▓▓▓▓▓▓▓▓▓▓▓▓▓▒▒▒▒ ",
|
|
128
|
+
" ░▒░ ▒▒▒▒▒▒▒▓▓▓▓▓▓▓▓▒▒▒▒▒░ ",
|
|
129
|
+
" ░▒▒▒▒░ ░▒▒▒▒░ ",
|
|
130
|
+
] as const;
|
|
131
|
+
|
|
132
|
+
const BTS_MASCOT_MAP = [
|
|
133
|
+
" mmmm mmmv ",
|
|
134
|
+
" lhhhhlv dmlhhhhp ",
|
|
135
|
+
" hhl mhhpv mlhhddhhl ",
|
|
136
|
+
" dhhv mphmvd dmmmmmvd mmphp pl ",
|
|
137
|
+
" dhhm dmmmmmmmmmmmmmmmmmmmmd pl ",
|
|
138
|
+
" hhl ddmmmmmmmmmmmmmmmmmmmmmmmvd dhhp ",
|
|
139
|
+
" vmvmmmmmmmmmmmmmmmmmmmmmmmmmmmmv ",
|
|
140
|
+
" dmmmmplhhhhhhmmmmmmmmmphhhhhhpmv ",
|
|
141
|
+
" vmmmphhhp lhhhhmmmlhhhh vhhhlm ",
|
|
142
|
+
" vmmhhhl dlhhmmmlhlv mlhhd d cc ",
|
|
143
|
+
" mhhhhd lhmmmlh dlpd hhbbbld ",
|
|
144
|
+
" lhhhhp hb mmmd lb hhhbhcbbbbbh ",
|
|
145
|
+
" mhhhpd vhhvvvlhv dhbbbccbccbbbl",
|
|
146
|
+
" dhv hhhh phhhl lhhbbcchccbhhv",
|
|
147
|
+
" plp lhhhhp plhhhld dp dlhbbbbcbhh ",
|
|
148
|
+
" pldddvhhhhhhhhhhhhhhhhhhhmdmlv dvmlhhd ",
|
|
149
|
+
" vmhhhhhhhhhhhhhm ",
|
|
150
|
+
" mmmmmd mmmmmmmllllllmmmmmv d ",
|
|
151
|
+
" dvmmmmvvvd dvmmmmmmmhhhhhhhhlmmmmmvv dvd d ",
|
|
152
|
+
"vmmmmm d vmmmd phhhhhhhhhhhmmmmmmmmmv d ",
|
|
153
|
+
"vmmvd vmmmm vmmmmv hhhhhhhhhhhmv vmmvd ",
|
|
154
|
+
" md dmmmmmv vmd hhhhhhhhhhhmmm ",
|
|
155
|
+
" mmmmv mmv dmhhhhhhhhhhhhmmm ",
|
|
156
|
+
" mmmmvd mmmmmmhhhhhhhhhhhhhmmmm ",
|
|
157
|
+
" vmd mmmmmmmhhhhhhhhmmmmmv ",
|
|
158
|
+
" dmmmmv vmmmmd ",
|
|
115
159
|
] as const;
|
|
116
160
|
|
|
117
161
|
const HEADER_SIMPLE = [
|
|
@@ -128,6 +172,33 @@ const HEADER_SIMPLE = [
|
|
|
128
172
|
" ░░░░░░ <ideas_compiler/>",
|
|
129
173
|
] as const;
|
|
130
174
|
|
|
175
|
+
const NEON_HEADER_ROWS = [
|
|
176
|
+
[" █████████ ████████ ", " ddddddddd dddddddd "],
|
|
177
|
+
[" ██████████████████████ ███████████ █████████████████████ ████████████", " dddddddddddpdddddddddd ddccccccddd ddddddddddddddddddddd ddccccccccpd"],
|
|
178
|
+
[" ██████████████████████████ ███████████ ████████████████████████████████████", " dddccchhhhhhhhhhhhhhhcpddd ddcchhhhmdd dddccchhhhhhhhhhhhhhhcdddphhhhhllddd"],
|
|
179
|
+
[" ███████████████████████████████████████████████████████████████████████████████ ████████████████████████████████████ ", " pddpchhhhhhhhhhhhhhhhhhhhmdddddddddddddddddddddddddddchhhhhmdddddddddpddddddddd ddpllhhhhhhllllllhhhhhhmdddddddddddd "],
|
|
180
|
+
[" ████████████████████████████████████████████████████████████████████████████████ ███████████████████████████████████ ", " ddpchhhhhhldddddddchhhhhlmddppchhhhhhhhhhhcdppcchhhhhhhhhhhldchhhhhpdddpchhhhcdd dddddcchhhhdddddpchhhhlddpcchhhcpdd "],
|
|
181
|
+
[" ██████████████ ████████████████████████████████████████████ ████████████████████████████████████████ ████████████████████ ", " dddcchhhhhlddd ddchhhhhldpcchhhhlmmmmmmmddcchhhhlmmlhhhhhhd chhhhlddddcchhhhddddddddddddddpchhhhhmdd dchhhhhmddpchhhhlddd "],
|
|
182
|
+
[" ██████████████ ███████████████████████████████████████████████████████████████████████████████████████████████████████████ ", " dddcchhhhhmddd pdcchhhhhlddpplhhhhlddddddddcchhhhldddchhhhlddchhhhhmddpchhhhhldcchhhhhhhlpdpcchhhhhhhhhhhhhmmdddpchhhhlddd "],
|
|
183
|
+
[" ████████████████████████████ █████████████████████████████████████████████████████████████████████████████████████████████ ", " ddcchhhhhldddddddpchhhhhhmdd dddmhhhhhldddddchhhhlmddpchhhhmddchhhhhhcccchhhhhmddlllllllllddpchhhhhlllllllldddddpcchhhhldd "],
|
|
184
|
+
["█████████████████████████████████████████████████████████████████████████████████████████████████████████████ ████████████████ ", "ddchhhhhhhlcccccccchhhhhhddpccccccchhhhhhddpchhhhhhcccchhhhlddddmhhhhhhhhhhhhhmdddddddddddddpchhhhldddddddddd ddchhhhhmddddddd "],
|
|
185
|
+
[" ███████████████████████████████████████████████████████████████████████████████ ██████████ ██████████████████ ", " ddmhhhhhhhhhhhhhhhhhhhddpchhhhhhhhhhhhmddddhhhhhhhhhhhhhhmdddddddddddpchhhhhmdd pchhhhhldd dddchhhhhhhhmddddd "],
|
|
186
|
+
[" █████████████████████████████████████ ████████████████████████████████████ ███████████ ███████████ █ ", " ddddddddddddddddddddddddddddddddddddd ddddddddddddddddddcchhhhhhhhhhhhmddd dpccldddddd dpchldddddd d "],
|
|
187
|
+
[" ██ █████████████████ ██████ ██████ ", " dd chhhhhhhhhhlddddd dddddd dddddd "],
|
|
188
|
+
[" ████████████████ ", " dddddddddddddddd "],
|
|
189
|
+
] as const;
|
|
190
|
+
|
|
191
|
+
const NEON_HEADER_TONES: MascotTonePalette = {
|
|
192
|
+
b: "#FFFFFF",
|
|
193
|
+
h: { kind: "theme", token: "accent" },
|
|
194
|
+
l: { kind: "theme", token: "accent", dimmed: true },
|
|
195
|
+
m: { kind: "theme", token: "accent", dimmed: true },
|
|
196
|
+
d: "#12011B",
|
|
197
|
+
p: { kind: "theme", token: "accent", dimmed: true },
|
|
198
|
+
c: "#FFFFFF",
|
|
199
|
+
v: "#FFFFFF",
|
|
200
|
+
};
|
|
201
|
+
|
|
131
202
|
type HeaderPalette = {
|
|
132
203
|
baseColor: string;
|
|
133
204
|
highlightColor: string;
|
|
@@ -148,7 +219,13 @@ export const MASCOT_TONE_KEYS = {
|
|
|
148
219
|
export type MascotToneKey =
|
|
149
220
|
(typeof MASCOT_TONE_KEYS)[keyof typeof MASCOT_TONE_KEYS];
|
|
150
221
|
|
|
151
|
-
export type
|
|
222
|
+
export type ThemeForegroundColor = {
|
|
223
|
+
kind: "theme";
|
|
224
|
+
token: string;
|
|
225
|
+
dimmed?: true;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export type MascotTonePalette = Record<MascotToneKey, string | ThemeForegroundColor>;
|
|
152
229
|
|
|
153
230
|
type HeaderVariantConfig = {
|
|
154
231
|
label: string;
|
|
@@ -162,18 +239,6 @@ type HeaderVariantConfig = {
|
|
|
162
239
|
mascotTonePalette?: MascotTonePalette;
|
|
163
240
|
};
|
|
164
241
|
|
|
165
|
-
const CLASSIC_PALETTE: HeaderPalette = {
|
|
166
|
-
baseColor: "accent",
|
|
167
|
-
highlightColor: "mdHeading",
|
|
168
|
-
trailColor: "mdLink",
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const CLASSIC_LINK_PALETTE: HeaderPalette = {
|
|
172
|
-
baseColor: "mdLink",
|
|
173
|
-
highlightColor: "mdHeading",
|
|
174
|
-
trailColor: "accent",
|
|
175
|
-
};
|
|
176
|
-
|
|
177
242
|
const OSDY_THEME_CYAN_PALETTE: HeaderPalette = {
|
|
178
243
|
baseColor: "accent",
|
|
179
244
|
highlightColor: "mdHeading",
|
|
@@ -209,6 +274,17 @@ const HTML_MASCOT_TONES: MascotTonePalette = {
|
|
|
209
274
|
v: "mdLink",
|
|
210
275
|
};
|
|
211
276
|
|
|
277
|
+
const BTS_MASCOT_TONES: MascotTonePalette = {
|
|
278
|
+
b: "#FCFCFC",
|
|
279
|
+
h: "#F9F3E7",
|
|
280
|
+
l: "#DDD5CC",
|
|
281
|
+
m: "#969291",
|
|
282
|
+
d: "#53445F",
|
|
283
|
+
p: "#C9BBA8",
|
|
284
|
+
c: "#CDA5F1",
|
|
285
|
+
v: "#746D6C",
|
|
286
|
+
};
|
|
287
|
+
|
|
212
288
|
const RAW_BODY_TONE_KEYS = [
|
|
213
289
|
MASCOT_TONE_KEYS.background,
|
|
214
290
|
MASCOT_TONE_KEYS.bright,
|
|
@@ -220,7 +296,8 @@ const RAW_HEX_COLOR = /^#[0-9A-F]{6}$/;
|
|
|
220
296
|
|
|
221
297
|
function assertRawHexPalette(palette: MascotTonePalette): void {
|
|
222
298
|
for (const tone of RAW_BODY_TONE_KEYS) {
|
|
223
|
-
|
|
299
|
+
const color = palette[tone];
|
|
300
|
+
if (typeof color !== "string" || !RAW_HEX_COLOR.test(color)) {
|
|
224
301
|
throw new Error(`Mascot tone ${tone} must be a #RRGGBB color.`);
|
|
225
302
|
}
|
|
226
303
|
}
|
|
@@ -378,9 +455,31 @@ const ROSE_MASCOT = addRightEdgeGlow({
|
|
|
378
455
|
mascot: HTML_MASCOT,
|
|
379
456
|
toneMap: HTML_MASCOT_MAP,
|
|
380
457
|
});
|
|
458
|
+
const BTS_MASCOT_ART = addRightEdgeGlow({
|
|
459
|
+
mascot: BTS_MASCOT,
|
|
460
|
+
toneMap: BTS_MASCOT_MAP,
|
|
461
|
+
});
|
|
462
|
+
for (const [art, palette] of [
|
|
463
|
+
[ROSE_MASCOT, HTML_MASCOT_TONES],
|
|
464
|
+
[BTS_MASCOT_ART, BTS_MASCOT_TONES],
|
|
465
|
+
] as const) {
|
|
466
|
+
assertRawHexPalette(palette);
|
|
467
|
+
assertMascotStructure(art, palette);
|
|
468
|
+
}
|
|
381
469
|
|
|
382
|
-
|
|
383
|
-
|
|
470
|
+
export type MascotConfig = {
|
|
471
|
+
art: MascotArt;
|
|
472
|
+
tonePalette: MascotTonePalette;
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
export const MASCOTS: Record<MascotChoice, MascotConfig> = {
|
|
476
|
+
current: { art: ROSE_MASCOT, tonePalette: HTML_MASCOT_TONES },
|
|
477
|
+
bts: { art: BTS_MASCOT_ART, tonePalette: BTS_MASCOT_TONES },
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
export function mascotForChoice(choice: MascotChoice): MascotConfig {
|
|
481
|
+
return MASCOTS[choice];
|
|
482
|
+
}
|
|
384
483
|
|
|
385
484
|
type PreparedMascotArt = {
|
|
386
485
|
art: MascotArt;
|
|
@@ -453,13 +552,14 @@ export const HEADER_VARIANTS: Record<HeaderVariant, HeaderVariantConfig> = {
|
|
|
453
552
|
mascotPalette: OSDY_THEME_MASCOT_PALETTE,
|
|
454
553
|
mascotTonePalette: HTML_MASCOT_TONES,
|
|
455
554
|
},
|
|
456
|
-
|
|
457
|
-
label: "
|
|
458
|
-
header:
|
|
555
|
+
neon: {
|
|
556
|
+
label: "Neon",
|
|
557
|
+
header: NEON_HEADER_ROWS.map(([header]) => header),
|
|
558
|
+
headerMap: NEON_HEADER_ROWS.map(([, toneMap]) => toneMap),
|
|
559
|
+
headerTonePalette: NEON_HEADER_TONES,
|
|
459
560
|
mascot: ROSE_MASCOT.mascot,
|
|
460
561
|
mascotMap: ROSE_MASCOT.toneMap,
|
|
461
|
-
linePalette:
|
|
462
|
-
lineIndex >= 6 ? CLASSIC_LINK_PALETTE : CLASSIC_PALETTE,
|
|
562
|
+
linePalette: osdyThemePalette,
|
|
463
563
|
mascotPalette: OSDY_THEME_MASCOT_PALETTE,
|
|
464
564
|
mascotTonePalette: HTML_MASCOT_TONES,
|
|
465
565
|
},
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { registerHooks } from "node:module";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
8
|
+
|
|
9
|
+
registerHooks({
|
|
10
|
+
resolve(specifier, context, nextResolve) {
|
|
11
|
+
if (
|
|
12
|
+
specifier.startsWith(".") &&
|
|
13
|
+
specifier.endsWith(".js") &&
|
|
14
|
+
context.parentURL?.endsWith(".ts")
|
|
15
|
+
) {
|
|
16
|
+
const sourceUrl = new URL(`${specifier.slice(0, -3)}.ts`, context.parentURL);
|
|
17
|
+
if (existsSync(fileURLToPath(sourceUrl))) {
|
|
18
|
+
return { shortCircuit: true, url: sourceUrl.href };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return nextResolve(specifier, context);
|
|
22
|
+
},
|
|
23
|
+
load(url, context, nextLoad) {
|
|
24
|
+
if (url.endsWith(".ts")) {
|
|
25
|
+
return {
|
|
26
|
+
format: "module",
|
|
27
|
+
shortCircuit: true,
|
|
28
|
+
source: ts.transpileModule(readFileSync(fileURLToPath(url), "utf8"), {
|
|
29
|
+
compilerOptions: {
|
|
30
|
+
module: ts.ModuleKind.ESNext,
|
|
31
|
+
target: ts.ScriptTarget.ES2022,
|
|
32
|
+
},
|
|
33
|
+
}).outputText,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return nextLoad(url, context);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const { formatContextUsage } = await import("./context-usage-ui.js");
|
|
41
|
+
|
|
42
|
+
const theme = {
|
|
43
|
+
fg(token: string, text: string): string {
|
|
44
|
+
return `<${token}>${text}</${token}>`;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
void test("formats the selected single-line usage shape", () => {
|
|
49
|
+
assert.equal(
|
|
50
|
+
formatContextUsage(theme, {
|
|
51
|
+
input: 0,
|
|
52
|
+
output: 0,
|
|
53
|
+
cacheRead: 0,
|
|
54
|
+
cacheWrite: 0,
|
|
55
|
+
cost: 0,
|
|
56
|
+
percent: 60,
|
|
57
|
+
contextWindow: 272_000,
|
|
58
|
+
}),
|
|
59
|
+
"tok ↑0 ↓0 · $0.0000 · ctx <warning>[██████░░░░]</warning> 60%/272.0k",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
void test("selects the full or compact usage line at the ANSI-aware width boundary", () => {
|
|
64
|
+
const ansiTheme = {
|
|
65
|
+
fg(_token: string, text: string): string {
|
|
66
|
+
return `\u001B[33m${text}\u001B[39m`;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const usage = {
|
|
70
|
+
input: 0,
|
|
71
|
+
output: 0,
|
|
72
|
+
cacheRead: 0,
|
|
73
|
+
cacheWrite: 0,
|
|
74
|
+
cost: 0,
|
|
75
|
+
percent: 60,
|
|
76
|
+
contextWindow: 272_000,
|
|
77
|
+
};
|
|
78
|
+
const full =
|
|
79
|
+
"tok ↑0 ↓0 · $0.0000 · ctx \u001B[33m[██████░░░░]\u001B[39m 60%/272.0k";
|
|
80
|
+
|
|
81
|
+
assert.equal(visibleWidth(full), 49);
|
|
82
|
+
assert.equal(formatContextUsage(ansiTheme, usage, 49), full);
|
|
83
|
+
assert.equal(
|
|
84
|
+
formatContextUsage(ansiTheme, usage, 48),
|
|
85
|
+
"$0.0000 · ctx \u001B[33m[██████░░░░]\u001B[39m 60%",
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
void test("formats a ten-cell green context bar at the inclusive 30% threshold", () => {
|
|
90
|
+
assert.equal(
|
|
91
|
+
formatContextUsage(theme, {
|
|
92
|
+
input: 0,
|
|
93
|
+
output: 0,
|
|
94
|
+
cacheRead: 0,
|
|
95
|
+
cacheWrite: 0,
|
|
96
|
+
cost: 0,
|
|
97
|
+
percent: 30,
|
|
98
|
+
contextWindow: 272_000,
|
|
99
|
+
}),
|
|
100
|
+
"tok ↑0 ↓0 · $0.0000 · ctx <success>[███░░░░░░░]</success> 30%/272.0k",
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
void test("uses warning and error colors for higher rounded usage", () => {
|
|
105
|
+
assert.equal(
|
|
106
|
+
formatContextUsage(theme, {
|
|
107
|
+
input: 1_500,
|
|
108
|
+
output: 2_500_000,
|
|
109
|
+
cacheRead: 12,
|
|
110
|
+
cacheWrite: 34,
|
|
111
|
+
cost: 1.2,
|
|
112
|
+
percent: 30.5,
|
|
113
|
+
contextWindow: 200_000,
|
|
114
|
+
}),
|
|
115
|
+
"tok ↑1.5k ↓2.5m R12 W34 · $1.2000 · ctx <warning>[███░░░░░░░]</warning> 31%/200.0k",
|
|
116
|
+
);
|
|
117
|
+
assert.equal(
|
|
118
|
+
formatContextUsage(theme, {
|
|
119
|
+
input: 0,
|
|
120
|
+
output: 0,
|
|
121
|
+
cacheRead: 0,
|
|
122
|
+
cacheWrite: 0,
|
|
123
|
+
cost: 1.2,
|
|
124
|
+
percent: 80.5,
|
|
125
|
+
contextWindow: 200_000,
|
|
126
|
+
}),
|
|
127
|
+
"tok ↑0 ↓0 · $1.2000 · ctx <error>[████████░░]</error> 81%/200.0k",
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
void test("clamps usage and safely represents missing context usage", () => {
|
|
132
|
+
assert.equal(
|
|
133
|
+
formatContextUsage(theme, {
|
|
134
|
+
input: 0,
|
|
135
|
+
output: 0,
|
|
136
|
+
cacheRead: 0,
|
|
137
|
+
cacheWrite: 0,
|
|
138
|
+
cost: 0,
|
|
139
|
+
percent: -10,
|
|
140
|
+
contextWindow: 200_000,
|
|
141
|
+
}),
|
|
142
|
+
"tok ↑0 ↓0 · $0.0000 · ctx <success>[░░░░░░░░░░]</success> 0%/200.0k",
|
|
143
|
+
);
|
|
144
|
+
assert.equal(
|
|
145
|
+
formatContextUsage(theme, {
|
|
146
|
+
input: 0,
|
|
147
|
+
output: 0,
|
|
148
|
+
cacheRead: 0,
|
|
149
|
+
cacheWrite: 0,
|
|
150
|
+
cost: 0,
|
|
151
|
+
percent: undefined,
|
|
152
|
+
contextWindow: 272_000,
|
|
153
|
+
}),
|
|
154
|
+
"tok ↑0 ↓0 · $0.0000 · ctx <success>[░░░░░░░░░░]</success> ?%/272.0k",
|
|
155
|
+
);
|
|
156
|
+
assert.equal(
|
|
157
|
+
formatContextUsage(theme, {
|
|
158
|
+
input: 0,
|
|
159
|
+
output: 0,
|
|
160
|
+
cacheRead: 0,
|
|
161
|
+
cacheWrite: 0,
|
|
162
|
+
cost: 0,
|
|
163
|
+
percent: undefined,
|
|
164
|
+
contextWindow: undefined,
|
|
165
|
+
}),
|
|
166
|
+
"tok ↑0 ↓0 · $0.0000 · ctx <success>[░░░░░░░░░░]</success> ?%/?",
|
|
167
|
+
);
|
|
168
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { shortNumber } from "./format.js";
|
|
3
|
+
|
|
4
|
+
export type ContextUsageDisplayData = {
|
|
5
|
+
input: number;
|
|
6
|
+
output: number;
|
|
7
|
+
cacheRead: number;
|
|
8
|
+
cacheWrite: number;
|
|
9
|
+
cost: number;
|
|
10
|
+
percent: number | undefined;
|
|
11
|
+
contextWindow: number | undefined;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type UsageTheme = {
|
|
15
|
+
fg(token: "success" | "warning" | "error", text: string): string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function roundedClampedPercent(
|
|
19
|
+
percent: number | undefined,
|
|
20
|
+
): number | undefined {
|
|
21
|
+
if (percent === undefined || !Number.isFinite(percent)) return undefined;
|
|
22
|
+
return Math.min(100, Math.max(0, Math.round(percent)));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function usageTone(percent: number): "success" | "warning" | "error" {
|
|
26
|
+
if (percent <= 30) return "success";
|
|
27
|
+
if (percent <= 80) return "warning";
|
|
28
|
+
return "error";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function usageBar(percent: number): string {
|
|
32
|
+
const filledCells = Math.round(percent / 10);
|
|
33
|
+
return "█".repeat(filledCells) + "░".repeat(10 - filledCells);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function formatContextUsage(
|
|
37
|
+
theme: UsageTheme,
|
|
38
|
+
usage: ContextUsageDisplayData,
|
|
39
|
+
availableWidth?: number,
|
|
40
|
+
): string {
|
|
41
|
+
const percent = roundedClampedPercent(usage.percent);
|
|
42
|
+
const displayPercent = percent ?? 0;
|
|
43
|
+
const percentageLabel = percent === undefined ? "?" : String(percent);
|
|
44
|
+
const contextWindowLabel =
|
|
45
|
+
usage.contextWindow === undefined || !Number.isFinite(usage.contextWindow)
|
|
46
|
+
? "?"
|
|
47
|
+
: shortNumber(usage.contextWindow);
|
|
48
|
+
const cacheLabel =
|
|
49
|
+
usage.cacheRead || usage.cacheWrite
|
|
50
|
+
? ` R${shortNumber(usage.cacheRead)} W${shortNumber(usage.cacheWrite)}`
|
|
51
|
+
: "";
|
|
52
|
+
const bar = theme.fg(
|
|
53
|
+
usageTone(displayPercent),
|
|
54
|
+
`[${usageBar(displayPercent)}]`,
|
|
55
|
+
);
|
|
56
|
+
const full = `tok ↑${shortNumber(usage.input)} ↓${shortNumber(usage.output)}${cacheLabel} · $${usage.cost.toFixed(4)} · ctx ${bar} ${percentageLabel}%/${contextWindowLabel}`;
|
|
57
|
+
if (availableWidth === undefined || visibleWidth(full) <= availableWidth)
|
|
58
|
+
return full;
|
|
59
|
+
return `$${usage.cost.toFixed(4)} · ctx ${bar} ${percentageLabel}%`;
|
|
60
|
+
}
|
|
@@ -4,10 +4,14 @@ import {
|
|
|
4
4
|
createEditorSettingsStore,
|
|
5
5
|
normalizeEditorSettings,
|
|
6
6
|
type EditorSettingsFileSystem,
|
|
7
|
-
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
7
|
+
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
8
8
|
} from "./editor-settings.ts";
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
EDITOR_MODES,
|
|
11
|
+
HEADER_VARIANT_CHOICES,
|
|
12
|
+
MASCOT_CHOICES,
|
|
13
|
+
// @ts-expect-error Node's native TypeScript runner resolves test-only TypeScript source imports.
|
|
14
|
+
} from "./types.ts";
|
|
11
15
|
|
|
12
16
|
class MemoryEditorSettingsFileSystem implements EditorSettingsFileSystem {
|
|
13
17
|
content: string | undefined;
|
|
@@ -41,35 +45,120 @@ class MemoryEditorSettingsFileSystem implements EditorSettingsFileSystem {
|
|
|
41
45
|
private pendingContent = "";
|
|
42
46
|
}
|
|
43
47
|
|
|
48
|
+
void test("editor settings preserve version-one header choices and fall back safely", async () => {
|
|
49
|
+
assert.deepEqual(HEADER_VARIANT_CHOICES, ["osdy-theme", "neon"]);
|
|
50
|
+
assert.equal(
|
|
51
|
+
normalizeEditorSettings({ version: 1 }).headerVariant,
|
|
52
|
+
"osdy-theme",
|
|
53
|
+
);
|
|
54
|
+
assert.equal(
|
|
55
|
+
normalizeEditorSettings({ version: 1, headerVariant: "classic" })
|
|
56
|
+
.headerVariant,
|
|
57
|
+
"osdy-theme",
|
|
58
|
+
);
|
|
59
|
+
assert.equal(
|
|
60
|
+
normalizeEditorSettings({ version: 1, headerVariant: "invalid" })
|
|
61
|
+
.headerVariant,
|
|
62
|
+
"osdy-theme",
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const fileSystem = new MemoryEditorSettingsFileSystem();
|
|
66
|
+
const store = createEditorSettingsStore(
|
|
67
|
+
"/agent/extensions/osdy-pi/settings.json",
|
|
68
|
+
fileSystem,
|
|
69
|
+
);
|
|
70
|
+
await store.save({
|
|
71
|
+
version: 1,
|
|
72
|
+
enabled: true,
|
|
73
|
+
editorMode: EDITOR_MODES.AUTO,
|
|
74
|
+
workingTreeEnabled: true,
|
|
75
|
+
headerVariant: "neon",
|
|
76
|
+
mascot: "current",
|
|
77
|
+
});
|
|
78
|
+
assert.equal((await store.load()).headerVariant, "neon");
|
|
79
|
+
});
|
|
80
|
+
|
|
44
81
|
void test("editor settings normalize only supported versioned modes", () => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
{
|
|
82
|
+
for (const [input, editorMode] of [
|
|
83
|
+
[{ version: 1, editorMode: "simple" }, EDITOR_MODES.SIMPLE],
|
|
84
|
+
[{ version: 1, editorMode: "invalid" }, EDITOR_MODES.AUTO],
|
|
85
|
+
[{ version: 2, editorMode: "extended" }, EDITOR_MODES.AUTO],
|
|
86
|
+
[null, EDITOR_MODES.AUTO],
|
|
87
|
+
] as const) {
|
|
88
|
+
assert.deepEqual(normalizeEditorSettings(input), {
|
|
48
89
|
version: 1,
|
|
49
|
-
|
|
90
|
+
enabled: true,
|
|
91
|
+
editorMode,
|
|
50
92
|
workingTreeEnabled: true,
|
|
51
|
-
|
|
93
|
+
headerVariant: "osdy-theme",
|
|
94
|
+
mascot: "current",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
void test("mascot choices preserve the version-one current default, migrate raccoon to Bts, and persist Bts", async () => {
|
|
100
|
+
assert.deepEqual(MASCOT_CHOICES, ["current", "bts"]);
|
|
101
|
+
assert.equal(
|
|
102
|
+
normalizeEditorSettings({ version: 1, mascot: "invalid" }).mascot,
|
|
103
|
+
"current",
|
|
52
104
|
);
|
|
53
|
-
assert.
|
|
54
|
-
normalizeEditorSettings({ version: 1,
|
|
55
|
-
|
|
56
|
-
version: 1,
|
|
57
|
-
editorMode: EDITOR_MODES.AUTO,
|
|
58
|
-
workingTreeEnabled: true,
|
|
59
|
-
},
|
|
105
|
+
assert.equal(
|
|
106
|
+
normalizeEditorSettings({ version: 1, mascot: "raccoon" }).mascot,
|
|
107
|
+
"bts",
|
|
60
108
|
);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
109
|
+
for (const mascot of ["detective", "explorer"] as const) {
|
|
110
|
+
assert.equal(
|
|
111
|
+
normalizeEditorSettings({ version: 1, mascot }).mascot,
|
|
112
|
+
"current",
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fileSystem = new MemoryEditorSettingsFileSystem();
|
|
117
|
+
const store = createEditorSettingsStore(
|
|
118
|
+
"/agent/extensions/osdy-pi/settings.json",
|
|
119
|
+
fileSystem,
|
|
68
120
|
);
|
|
69
|
-
|
|
121
|
+
await store.save({
|
|
70
122
|
version: 1,
|
|
123
|
+
enabled: true,
|
|
71
124
|
editorMode: EDITOR_MODES.AUTO,
|
|
72
125
|
workingTreeEnabled: true,
|
|
126
|
+
headerVariant: "neon",
|
|
127
|
+
mascot: "bts",
|
|
128
|
+
});
|
|
129
|
+
assert.equal((await store.load()).mascot, "bts");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
void test("enabled defaults safely and persists globally", async () => {
|
|
133
|
+
assert.equal(
|
|
134
|
+
normalizeEditorSettings({ version: 1, editorMode: "simple" }).enabled,
|
|
135
|
+
true,
|
|
136
|
+
);
|
|
137
|
+
assert.equal(
|
|
138
|
+
normalizeEditorSettings({ version: 1, enabled: "no" }).enabled,
|
|
139
|
+
true,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
const fileSystem = new MemoryEditorSettingsFileSystem();
|
|
143
|
+
const store = createEditorSettingsStore(
|
|
144
|
+
"/agent/extensions/osdy-pi/settings.json",
|
|
145
|
+
fileSystem,
|
|
146
|
+
);
|
|
147
|
+
await store.save({
|
|
148
|
+
version: 1,
|
|
149
|
+
enabled: false,
|
|
150
|
+
editorMode: EDITOR_MODES.SIMPLE,
|
|
151
|
+
workingTreeEnabled: false,
|
|
152
|
+
headerVariant: "osdy-theme",
|
|
153
|
+
mascot: "current",
|
|
154
|
+
});
|
|
155
|
+
assert.deepEqual(await store.load(), {
|
|
156
|
+
version: 1,
|
|
157
|
+
enabled: false,
|
|
158
|
+
editorMode: EDITOR_MODES.SIMPLE,
|
|
159
|
+
workingTreeEnabled: false,
|
|
160
|
+
headerVariant: "osdy-theme",
|
|
161
|
+
mascot: "current",
|
|
73
162
|
});
|
|
74
163
|
});
|
|
75
164
|
|
|
@@ -98,8 +187,11 @@ void test("working-tree visibility defaults safely and persists globally", async
|
|
|
98
187
|
);
|
|
99
188
|
await firstStore.save({
|
|
100
189
|
version: 1,
|
|
190
|
+
enabled: true,
|
|
101
191
|
editorMode: EDITOR_MODES.AUTO,
|
|
102
192
|
workingTreeEnabled: false,
|
|
193
|
+
headerVariant: "osdy-theme",
|
|
194
|
+
mascot: "current",
|
|
103
195
|
});
|
|
104
196
|
const reloadedStore = createEditorSettingsStore(
|
|
105
197
|
"/agent/extensions/osdy-pi/settings.json",
|
|
@@ -125,8 +217,11 @@ void test("editor settings load safely and save atomically", async () => {
|
|
|
125
217
|
|
|
126
218
|
await store.save({
|
|
127
219
|
version: 1,
|
|
220
|
+
enabled: true,
|
|
128
221
|
editorMode: EDITOR_MODES.SIMPLE,
|
|
129
222
|
workingTreeEnabled: true,
|
|
223
|
+
headerVariant: "osdy-theme",
|
|
224
|
+
mascot: "current",
|
|
130
225
|
});
|
|
131
226
|
assert.deepEqual(fileSystem.mkdirPaths, ["/agent/extensions/osdy-pi"]);
|
|
132
227
|
assert.deepEqual(fileSystem.writePaths, [
|
|
@@ -152,8 +247,11 @@ void test("new editor settings stores load their own persisted mode", async () =
|
|
|
152
247
|
|
|
153
248
|
await firstStore.save({
|
|
154
249
|
version: 1,
|
|
250
|
+
enabled: true,
|
|
155
251
|
editorMode: EDITOR_MODES.EXTENDED,
|
|
156
252
|
workingTreeEnabled: true,
|
|
253
|
+
headerVariant: "osdy-theme",
|
|
254
|
+
mascot: "current",
|
|
157
255
|
});
|
|
158
256
|
const reloadedStore = createEditorSettingsStore(
|
|
159
257
|
"/agent/extensions/osdy-pi/settings.json",
|