@underactive/pi-topping 0.6.5 → 0.7.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/README.md +10 -6
- package/package.json +1 -1
- package/src/activity-meter.ts +3 -3
- package/src/format.ts +32 -12
- package/src/menu.ts +27 -39
- package/src/pi-installer.ts +15 -6
- package/src/preview.ts +18 -17
- package/src/prompt-decorator.ts +31 -17
- package/src/session.ts +50 -45
- package/src/settings.ts +79 -55
- package/src/setup-command.ts +94 -48
- package/src/toppings.ts +67 -18
- package/src/word-packs.ts +1 -1
- package/src/words.ts +2 -10
package/src/session.ts
CHANGED
|
@@ -18,6 +18,8 @@ import {
|
|
|
18
18
|
buildWorkingMessage,
|
|
19
19
|
DEFAULT_WORKING_WORD,
|
|
20
20
|
dimAttribute,
|
|
21
|
+
getThinkingLevelColorizer,
|
|
22
|
+
isThinkingLevel,
|
|
21
23
|
ELAPSED_INTERVAL_MS,
|
|
22
24
|
fadeThemeColorString,
|
|
23
25
|
formatElapsed,
|
|
@@ -34,11 +36,12 @@ import {
|
|
|
34
36
|
TOKEN_RATE_FADE_SHADE_COUNT,
|
|
35
37
|
TOKEN_RATE_PLACEHOLDER,
|
|
36
38
|
StreamingWordCounter,
|
|
39
|
+
type ThinkingLevel,
|
|
37
40
|
} from "./format.ts";
|
|
38
41
|
import { isSiblingSetupEnabled } from "./flags.ts";
|
|
39
42
|
import { showMenu } from "./menu.ts";
|
|
40
43
|
import { registerSetupCommand } from "./setup-command.ts";
|
|
41
|
-
import { applyMenuResult, buildMenuSections, loadSettings, saveSettings, type
|
|
44
|
+
import { applyMenuResult, buildMenuSections, loadSettings, saveSettings, type ThinkingLevelColor } from "./settings.ts";
|
|
42
45
|
import { notifyMissingToppingsOnce } from "./toppings.ts";
|
|
43
46
|
import { PreviewRenderer } from "./preview.ts";
|
|
44
47
|
import { buildCompletionMarkerContent, buildCompletionMarkerLine, PROMPT_BOX_TYPE, promptBoxRenderer, type PromptBoxDetails } from "./prompt-decorator.ts";
|
|
@@ -73,6 +76,7 @@ interface DoneEntryData {
|
|
|
73
76
|
elapsedMs: number;
|
|
74
77
|
tokens?: number;
|
|
75
78
|
midTurnInputs?: number;
|
|
79
|
+
thinkingLevel?: ThinkingLevel;
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
interface SessionState {
|
|
@@ -199,6 +203,7 @@ export class SessionManager {
|
|
|
199
203
|
model: ctx.model?.id,
|
|
200
204
|
borderColor: this.#settings.decorations.borderColor,
|
|
201
205
|
borderStyle: this.#settings.decorations.borderStyle,
|
|
206
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
202
207
|
},
|
|
203
208
|
},
|
|
204
209
|
{ triggerTurn: true },
|
|
@@ -217,6 +222,7 @@ export class SessionManager {
|
|
|
217
222
|
if (!this.usable(ctx)) return;
|
|
218
223
|
if (!this.#state.startTime) this.resetTurn(Date.now());
|
|
219
224
|
this.#state.busy = true;
|
|
225
|
+
if (!this.#state.waiting) this.applyIndicator(ctx);
|
|
220
226
|
this.startTimer();
|
|
221
227
|
this.tick();
|
|
222
228
|
};
|
|
@@ -232,19 +238,17 @@ export class SessionManager {
|
|
|
232
238
|
|
|
233
239
|
#onMessageUpdate = async (event: MessageUpdateEvent, ctx: ExtensionContext): Promise<void> => {
|
|
234
240
|
this.#currentCtx = ctx;
|
|
241
|
+
if (!this.usable(ctx)) return;
|
|
235
242
|
const assistantEvent = event.assistantMessageEvent;
|
|
236
243
|
if (
|
|
237
|
-
this.usable(ctx) &&
|
|
238
244
|
assistantEvent &&
|
|
239
245
|
(assistantEvent.type === "text_delta" || assistantEvent.type === "thinking_delta")
|
|
240
246
|
) {
|
|
241
247
|
this.#state.liveTokens += this.#counter.count(assistantEvent.delta, assistantEvent.type);
|
|
242
248
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
this.updateResponseModel(rawResponseModel);
|
|
247
|
-
}
|
|
249
|
+
const rawResponseModel = (assistantEvent as { partial?: { responseModel?: unknown } } | undefined)?.partial?.responseModel
|
|
250
|
+
?? (event.message as { responseModel?: unknown } | undefined)?.responseModel;
|
|
251
|
+
this.updateResponseModel(rawResponseModel);
|
|
248
252
|
};
|
|
249
253
|
|
|
250
254
|
#onMessageEnd = async (event: MessageEndEvent, ctx: ExtensionContext): Promise<void> => {
|
|
@@ -319,6 +323,7 @@ export class SessionManager {
|
|
|
319
323
|
elapsedMs,
|
|
320
324
|
tokens: this.#state.confirmTokens,
|
|
321
325
|
midTurnInputs: this.#state.midTurnInputs,
|
|
326
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
322
327
|
});
|
|
323
328
|
}
|
|
324
329
|
this.#currentCtx = null;
|
|
@@ -336,8 +341,8 @@ export class SessionManager {
|
|
|
336
341
|
const elapsedMs = typeof entry.data.elapsedMs === "number" && Number.isFinite(entry.data.elapsedMs) ? entry.data.elapsedMs : 0;
|
|
337
342
|
const features = this.#settings.features;
|
|
338
343
|
const details: string[] = [];
|
|
339
|
-
if (features.doneMarkerTokens && typeof entry.data.tokens === "number") details.push(`↓ ${formatTokens(entry.data.tokens)} tokens`);
|
|
340
|
-
if (features.doneMarkerInputs && typeof entry.data.midTurnInputs === "number" && entry.data.midTurnInputs) {
|
|
344
|
+
if (features.doneMarkerTokens && typeof entry.data.tokens === "number" && Number.isFinite(entry.data.tokens) && entry.data.tokens >= 0) details.push(`↓ ${formatTokens(entry.data.tokens)} tokens`);
|
|
345
|
+
if (features.doneMarkerInputs && typeof entry.data.midTurnInputs === "number" && Number.isInteger(entry.data.midTurnInputs) && entry.data.midTurnInputs > 0) {
|
|
341
346
|
details.push(`${entry.data.midTurnInputs} mid-turn input${entry.data.midTurnInputs === 1 ? "" : "s"}`);
|
|
342
347
|
}
|
|
343
348
|
const icon = features.doneMarkerIcon ? theme.fg("text", this.#settings.decorations.useNerdFont ? "" : "π") : "";
|
|
@@ -345,6 +350,7 @@ export class SessionManager {
|
|
|
345
350
|
const borderStyle = this.#settings.decorations.doneMarkerBorderStyle;
|
|
346
351
|
const borderColor = this.#settings.decorations.doneMarkerBorderColor;
|
|
347
352
|
const markerStyle = this.#settings.decorations.doneMarkerStyle;
|
|
353
|
+
const thinkingLevel = isThinkingLevel(entry.data.thinkingLevel) ? entry.data.thinkingLevel : undefined;
|
|
348
354
|
if (borderStyle === "none") return new Text(markerContent);
|
|
349
355
|
|
|
350
356
|
let cachedWidth: number | undefined;
|
|
@@ -353,7 +359,7 @@ export class SessionManager {
|
|
|
353
359
|
render(width: number): string[] {
|
|
354
360
|
if (cachedWidth !== width) {
|
|
355
361
|
cachedWidth = width;
|
|
356
|
-
cachedLines = ["", buildCompletionMarkerLine(markerContent, width, theme, borderStyle, borderColor, markerStyle), ""];
|
|
362
|
+
cachedLines = ["", buildCompletionMarkerLine(markerContent, width, theme, borderStyle, borderColor, markerStyle, thinkingLevel), ""];
|
|
357
363
|
}
|
|
358
364
|
return cachedLines!;
|
|
359
365
|
},
|
|
@@ -412,11 +418,6 @@ export class SessionManager {
|
|
|
412
418
|
return this.#settings.decorations.animatedSpinner && this.#settings.loaderOrder[0] !== "spinner";
|
|
413
419
|
}
|
|
414
420
|
|
|
415
|
-
private spinnerColor(): SettingColor {
|
|
416
|
-
const decorations = this.#settings.decorations;
|
|
417
|
-
return decorations.spinnerColorEnabled ? decorations.spinnerColor : "accent";
|
|
418
|
-
}
|
|
419
|
-
|
|
420
421
|
private indicatorFingerprint(): string {
|
|
421
422
|
const decorations = this.#settings.decorations;
|
|
422
423
|
return `${decorations.animatedSpinner}:${decorations.spinnerColor}:${decorations.spinnerColorEnabled}:${this.#settings.loaderOrder[0]}`;
|
|
@@ -435,12 +436,10 @@ export class SessionManager {
|
|
|
435
436
|
return;
|
|
436
437
|
}
|
|
437
438
|
|
|
438
|
-
const color = this.spinnerColor
|
|
439
|
-
|
|
440
|
-
ctx.ui.
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
ctx.ui.setWorkingIndicator({ frames: SPINNER_FRAMES.map((frame) => ctx.ui.theme.fg(color, frame)) });
|
|
439
|
+
const color = this.#settings.decorations.spinnerColor;
|
|
440
|
+
ctx.ui.setWorkingIndicator({
|
|
441
|
+
frames: SPINNER_FRAMES.map((frame) => getThinkingLevelColorizer(ctx.ui.theme, color, ctx.thinkingLevel)(frame)),
|
|
442
|
+
});
|
|
444
443
|
}
|
|
445
444
|
|
|
446
445
|
private stopTimer(): void {
|
|
@@ -485,13 +484,19 @@ export class SessionManager {
|
|
|
485
484
|
}
|
|
486
485
|
}
|
|
487
486
|
|
|
488
|
-
private startResponseModelFade(ctx: ExtensionContext, model: string, color:
|
|
487
|
+
private startResponseModelFade(ctx: ExtensionContext, model: string, color: ThinkingLevelColor, dimmed: boolean): void {
|
|
489
488
|
this.cancelResponseModelFade(ctx);
|
|
490
489
|
const generation = this.#state.responseModelFadeGeneration;
|
|
490
|
+
const colorizer = getThinkingLevelColorizer(ctx.ui.theme, color, ctx.thinkingLevel);
|
|
491
491
|
const render = (shade?: number): void => {
|
|
492
492
|
const colored = shade === undefined
|
|
493
|
-
?
|
|
494
|
-
: fadeThemeColorString(
|
|
493
|
+
? colorizer(model)
|
|
494
|
+
: fadeThemeColorString(
|
|
495
|
+
model,
|
|
496
|
+
shade,
|
|
497
|
+
ctx.ui.theme,
|
|
498
|
+
colorizer,
|
|
499
|
+
);
|
|
495
500
|
const responseModel = dimmed ? dimAttribute(colored) : colored;
|
|
496
501
|
ctx.ui.setStatus(RESPONSE_MODEL_STATUS_KEY, responseModel);
|
|
497
502
|
};
|
|
@@ -573,10 +578,10 @@ export class SessionManager {
|
|
|
573
578
|
state.lastTokenRateSampledAt = now;
|
|
574
579
|
}
|
|
575
580
|
const spinner = this.spinnerInMessage()
|
|
576
|
-
? ctx.ui.theme.
|
|
581
|
+
? getThinkingLevelColorizer(ctx.ui.theme, decorations.spinnerColor, ctx.thinkingLevel)(SPINNER_FRAMES[Math.floor(now / SPINNER_FRAME_MS) % SPINNER_FRAMES.length]!)
|
|
577
582
|
: "";
|
|
578
583
|
const responseModelColored = features.responseModel && state.responseModel
|
|
579
|
-
? ctx.ui.theme
|
|
584
|
+
? getThinkingLevelColorizer(ctx.ui.theme, decorations.responseModelColor, ctx.thinkingLevel)(state.responseModel)
|
|
580
585
|
: "";
|
|
581
586
|
const responseModel = responseModelColored && decorations.responseModelDimmed ? dimAttribute(responseModelColored) : responseModelColored;
|
|
582
587
|
if (isFullyDefaultAppearance(features, decorations)) {
|
|
@@ -594,17 +599,13 @@ export class SessionManager {
|
|
|
594
599
|
const styled = decorations.shimmer
|
|
595
600
|
? shimmerString(word, now - state.shimmerOrigin, ctx.ui.theme, decorations.shimmerDirection, decorations.shimmerSpeed, decorations.shimmerInverted)
|
|
596
601
|
: ctx.ui.theme.fg("text", word);
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
decorations.meterDimmed,
|
|
605
|
-
),
|
|
606
|
-
)
|
|
607
|
-
: "";
|
|
602
|
+
let meter = "";
|
|
603
|
+
if (decorations.tokenActivityMonitor) {
|
|
604
|
+
const meterColorizer = getThinkingLevelColorizer(ctx.ui.theme, decorations.meterColor, ctx.thinkingLevel);
|
|
605
|
+
meter = state.activityMeter.render((level, char) =>
|
|
606
|
+
ActivityMeter.colorizeCell(level, char, ctx.ui.theme, meterColorizer, decorations.meterDimmed),
|
|
607
|
+
);
|
|
608
|
+
}
|
|
608
609
|
let tokenRateText = "";
|
|
609
610
|
if (features.tokenRate) {
|
|
610
611
|
const latestTokenRate = formatTokenRate(state.rateTracker.tokenRate);
|
|
@@ -616,18 +617,22 @@ export class SessionManager {
|
|
|
616
617
|
}
|
|
617
618
|
tokenRateText = state.tokenRateText;
|
|
618
619
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
620
|
+
let tokenRateSegment = "";
|
|
621
|
+
if (features.tokenRate) {
|
|
622
|
+
if (!tokenRateText) {
|
|
623
|
+
tokenRateSegment = ctx.ui.theme.fg("dim", TOKEN_RATE_PLACEHOLDER);
|
|
624
|
+
} else {
|
|
625
|
+
const tokenRateColorizer = getThinkingLevelColorizer(ctx.ui.theme, decorations.tokenRateColor, ctx.thinkingLevel);
|
|
626
|
+
tokenRateSegment = now < state.tokenRateFadeStartsAt
|
|
627
|
+
? tokenRateColorizer(tokenRateText)
|
|
625
628
|
: fadeThemeColorString(
|
|
626
629
|
tokenRateText,
|
|
627
630
|
Math.floor((now - state.tokenRateFadeStartsAt) / (TOKEN_RATE_FADE_MS / TOKEN_RATE_FADE_SHADE_COUNT)),
|
|
628
631
|
ctx.ui.theme,
|
|
629
|
-
|
|
632
|
+
tokenRateColorizer,
|
|
630
633
|
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
631
636
|
const tokenRateStyled = tokenRateSegment && decorations.tokenRateDimmed ? dimAttribute(tokenRateSegment) : tokenRateSegment;
|
|
632
637
|
const msg = buildWorkingMessage(
|
|
633
638
|
ctx.ui.theme,
|
|
@@ -657,7 +662,7 @@ export class SessionManager {
|
|
|
657
662
|
this.#userPacks = loadUserWordPacks();
|
|
658
663
|
this.#allPacks = [...this.#bundledPacks, ...this.#userPacks];
|
|
659
664
|
const before = this.indicatorFingerprint();
|
|
660
|
-
const preview = new PreviewRenderer(ctx,
|
|
665
|
+
const preview = new PreviewRenderer(ctx, this.#allPacks);
|
|
661
666
|
const result = await showMenu<Record<string, boolean | string>>(ctx, {
|
|
662
667
|
title: "Pi Topping: Settings",
|
|
663
668
|
maxHeight: "75%",
|
package/src/settings.ts
CHANGED
|
@@ -7,7 +7,20 @@ import { isWordPackEnabled, isWordPackId, type WordPack } from "./word-packs.ts"
|
|
|
7
7
|
import { isPlainObject } from "./util.ts";
|
|
8
8
|
|
|
9
9
|
export const SETTING_COLOR_VALUES = ["accent", "border", "borderAccent", "success", "error", "warning"] as const;
|
|
10
|
-
export
|
|
10
|
+
export const LOADER_COLOR_VALUES = [...SETTING_COLOR_VALUES, "text", "muted"] as const;
|
|
11
|
+
export const THINKING_LEVEL_COLOR_VALUES = ["thinking-level", ...LOADER_COLOR_VALUES] as const;
|
|
12
|
+
export type ThinkingLevelColor = (typeof THINKING_LEVEL_COLOR_VALUES)[number];
|
|
13
|
+
export const THINKING_LEVEL_SETTING_COLOR_VALUES = ["thinking-level", ...SETTING_COLOR_VALUES] as const;
|
|
14
|
+
export type ThinkingLevelSettingColor = (typeof THINKING_LEVEL_SETTING_COLOR_VALUES)[number];
|
|
15
|
+
export const SPINNER_COLOR_VALUES = THINKING_LEVEL_SETTING_COLOR_VALUES;
|
|
16
|
+
export type SpinnerColor = ThinkingLevelSettingColor;
|
|
17
|
+
export const PROMPT_BORDER_COLOR_VALUES = THINKING_LEVEL_SETTING_COLOR_VALUES;
|
|
18
|
+
export type PromptBorderColor = ThinkingLevelSettingColor;
|
|
19
|
+
export const DONE_MARKER_BORDER_COLOR_VALUES = THINKING_LEVEL_SETTING_COLOR_VALUES;
|
|
20
|
+
export type DoneMarkerBorderColor = ThinkingLevelSettingColor;
|
|
21
|
+
const THINKING_LEVEL_COLOR_MIGRATION_VERSION = 2;
|
|
22
|
+
const DONE_MARKER_BORDER_COLOR_MIGRATION_VERSION = 3;
|
|
23
|
+
export const SETTINGS_SCHEMA_VERSION = 3;
|
|
11
24
|
|
|
12
25
|
export const BORDER_STYLE_VALUES = ["double", "single", "rounded", "heavy"] as const;
|
|
13
26
|
export type BorderStyle = (typeof BORDER_STYLE_VALUES)[number];
|
|
@@ -28,10 +41,18 @@ export function isDoneMarkerStyle(value: unknown): value is DoneMarkerStyle {
|
|
|
28
41
|
return typeof value === "string" && DONE_MARKER_STYLE_VALUES.some(style => style === value);
|
|
29
42
|
}
|
|
30
43
|
|
|
31
|
-
export function
|
|
32
|
-
return typeof value === "string" &&
|
|
44
|
+
export function isThinkingLevelColor(value: unknown): value is ThinkingLevelColor {
|
|
45
|
+
return typeof value === "string" && THINKING_LEVEL_COLOR_VALUES.some(color => color === value);
|
|
33
46
|
}
|
|
34
47
|
|
|
48
|
+
export function isThinkingLevelSettingColor(value: unknown): value is ThinkingLevelSettingColor {
|
|
49
|
+
return typeof value === "string" && THINKING_LEVEL_SETTING_COLOR_VALUES.some(color => color === value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const isSpinnerColor = isThinkingLevelSettingColor;
|
|
53
|
+
export const isPromptBorderColor = isThinkingLevelSettingColor;
|
|
54
|
+
export const isDoneMarkerBorderColor = isThinkingLevelSettingColor;
|
|
55
|
+
|
|
35
56
|
export interface DecoratorSettings {
|
|
36
57
|
decorations: {
|
|
37
58
|
animatedSpinner: boolean;
|
|
@@ -45,21 +66,21 @@ export interface DecoratorSettings {
|
|
|
45
66
|
meterDirection: "ltr" | "rtl";
|
|
46
67
|
meterDirectionEnabled: boolean;
|
|
47
68
|
decorateUserPrompt: boolean;
|
|
48
|
-
borderColor:
|
|
69
|
+
borderColor: PromptBorderColor;
|
|
49
70
|
borderColorEnabled: boolean;
|
|
50
71
|
borderStyle: BorderStyle;
|
|
51
72
|
borderStyleEnabled: boolean;
|
|
52
73
|
doneMarkerBorderStyle: DoneMarkerBorderStyle;
|
|
53
|
-
doneMarkerBorderColor:
|
|
74
|
+
doneMarkerBorderColor: DoneMarkerBorderColor;
|
|
54
75
|
doneMarkerStyle: DoneMarkerStyle;
|
|
55
|
-
spinnerColor:
|
|
76
|
+
spinnerColor: SpinnerColor;
|
|
56
77
|
spinnerColorEnabled: boolean;
|
|
57
|
-
meterColor:
|
|
78
|
+
meterColor: ThinkingLevelColor;
|
|
58
79
|
meterColorEnabled: boolean;
|
|
59
80
|
meterDimmed: boolean;
|
|
60
|
-
tokenRateColor:
|
|
81
|
+
tokenRateColor: ThinkingLevelColor;
|
|
61
82
|
tokenRateDimmed: boolean;
|
|
62
|
-
responseModelColor:
|
|
83
|
+
responseModelColor: ThinkingLevelColor;
|
|
63
84
|
responseModelDimmed: boolean;
|
|
64
85
|
promptIcon: boolean;
|
|
65
86
|
promptTimestamp: boolean;
|
|
@@ -84,7 +105,7 @@ export interface DecoratorSettings {
|
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
export const DEFAULT_SETTINGS: DecoratorSettings = {
|
|
87
|
-
decorations: { animatedSpinner: true, shimmer: true, shimmerInverted: false, shimmerDirection: "ltr", shimmerDirectionEnabled: true, shimmerSpeed: "normal", shimmerSpeedEnabled: true, tokenActivityMonitor: true, meterDirection: "rtl", meterDirectionEnabled: true, decorateUserPrompt: true, borderColor: "
|
|
108
|
+
decorations: { animatedSpinner: true, shimmer: true, shimmerInverted: false, shimmerDirection: "ltr", shimmerDirectionEnabled: true, shimmerSpeed: "normal", shimmerSpeedEnabled: true, tokenActivityMonitor: true, meterDirection: "rtl", meterDirectionEnabled: true, decorateUserPrompt: true, borderColor: "thinking-level", borderColorEnabled: true, borderStyle: "double", borderStyleEnabled: true, doneMarkerBorderStyle: "none", doneMarkerBorderColor: "thinking-level", doneMarkerStyle: "elite", spinnerColor: "thinking-level", spinnerColorEnabled: true, meterColor: "accent", meterColorEnabled: true, meterDimmed: false, tokenRateColor: "warning", tokenRateDimmed: false, responseModelColor: "accent", responseModelDimmed: false, promptIcon: true, promptTimestamp: true, promptProvider: true, promptModel: true, useNerdFont: true },
|
|
88
109
|
features: { substituteDefaultMessage: true, elapsedTime: true, outputTokens: true, tokenRate: true, responseModel: true, doneMarker: true, doneMarkerIcon: true, randomizeDoneMarker: true, doneMarkerTokens: true, doneMarkerInputs: true },
|
|
89
110
|
loaderOrder: [...DEFAULT_LOADER_ORDER],
|
|
90
111
|
wordPacks: {},
|
|
@@ -128,7 +149,12 @@ function mergeGroup<T extends Record<string, boolean | string>>(defaults: T, par
|
|
|
128
149
|
if (typeof merged[key] === "boolean" && typeof value === "boolean") valid = value;
|
|
129
150
|
else if ((key === "meterDirection" || key === "shimmerDirection") && (value === "ltr" || value === "rtl")) valid = value;
|
|
130
151
|
else if (key === "shimmerSpeed" && (value === "slow" || value === "normal" || value === "fast")) valid = value;
|
|
131
|
-
else if (key
|
|
152
|
+
else if (key === "spinnerColor" && value === "default") valid = "thinking-level";
|
|
153
|
+
else if (key === "spinnerColor" && isSpinnerColor(value)) valid = value;
|
|
154
|
+
else if (key === "borderColor" && isPromptBorderColor(value)) valid = value;
|
|
155
|
+
else if (key === "doneMarkerBorderColor" && value === "default") valid = "thinking-level";
|
|
156
|
+
else if (key === "doneMarkerBorderColor" && isDoneMarkerBorderColor(value)) valid = value;
|
|
157
|
+
else if ((key === "meterColor" || key === "tokenRateColor" || key === "responseModelColor") && isThinkingLevelColor(value)) valid = value;
|
|
132
158
|
else if (key === "borderStyle" && isBorderStyle(value)) valid = value;
|
|
133
159
|
else if (key === "doneMarkerBorderStyle" && isDoneMarkerBorderStyle(value)) valid = value;
|
|
134
160
|
else if (key === "doneMarkerStyle" && isDoneMarkerStyle(value)) valid = value;
|
|
@@ -137,36 +163,14 @@ function mergeGroup<T extends Record<string, boolean | string>>(defaults: T, par
|
|
|
137
163
|
return merged;
|
|
138
164
|
}
|
|
139
165
|
|
|
140
|
-
/** Map a persisted setting value to its menu cycle label (directions and speeds only; colors pass through). */
|
|
141
|
-
function toCycleValue(stored: unknown): string {
|
|
142
|
-
if (stored === "ltr") return "Left to Right";
|
|
143
|
-
if (stored === "rtl") return "Right to Left";
|
|
144
|
-
if (stored === "slow") return "Slow";
|
|
145
|
-
if (stored === "normal") return "Normal";
|
|
146
|
-
if (stored === "fast") return "Fast";
|
|
147
|
-
return String(stored);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** Map a menu cycle label back to its persisted value. */
|
|
151
|
-
function fromCycleValue(value: string): string {
|
|
152
|
-
if (value === "Left to Right") return "ltr";
|
|
153
|
-
if (value === "Right to Left") return "rtl";
|
|
154
|
-
if (value === "Slow") return "slow";
|
|
155
|
-
if (value === "Normal") return "normal";
|
|
156
|
-
if (value === "Fast") return "fast";
|
|
157
|
-
return value;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
166
|
/** Convert a persisted or menu direction to a safe preview direction. */
|
|
161
167
|
export function fromCycleDirection(value: unknown): "ltr" | "rtl" {
|
|
162
|
-
|
|
163
|
-
return stored === "rtl" ? "rtl" : "ltr";
|
|
168
|
+
return value === "rtl" ? "rtl" : "ltr";
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
/** Convert a persisted or menu speed to a safe preview speed. */
|
|
167
172
|
export function fromCycleSpeed(value: unknown): "slow" | "normal" | "fast" {
|
|
168
|
-
|
|
169
|
-
return stored === "slow" || stored === "fast" ? stored : "normal";
|
|
173
|
+
return value === "slow" || value === "fast" ? value : "normal";
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
type MenuSectionName = "User Prompt" | "“Working” Loader" | "Completion Marker" | "Options";
|
|
@@ -174,42 +178,45 @@ type DecorationSettings = DecoratorSettings["decorations"];
|
|
|
174
178
|
type FeatureSettings = DecoratorSettings["features"];
|
|
175
179
|
type DecorationBooleanKey = { [Key in keyof DecorationSettings]: DecorationSettings[Key] extends boolean ? Key : never }[keyof DecorationSettings];
|
|
176
180
|
type MenuEntryBase = { id: string; label: string; section: MenuSectionName };
|
|
177
|
-
type DecorationMenuEntry = MenuEntryBase & { group: "decorations"; key: keyof DecorationSettings; cycleValues?: readonly string[]; cycleEnabledBy?: DecorationBooleanKey; cycleDisabledValue?: string };
|
|
178
|
-
type FeatureMenuEntry = MenuEntryBase & { group: "features"; key: keyof FeatureSettings; cycleValues?: never; cycleEnabledBy?: never; cycleDisabledValue?: never };
|
|
181
|
+
type DecorationMenuEntry = MenuEntryBase & { group: "decorations"; key: keyof DecorationSettings; cycleValues?: readonly string[]; cycleValueLabels?: Readonly<Record<string, string>>; cycleEnabledBy?: DecorationBooleanKey; cycleDisabledValue?: string };
|
|
182
|
+
type FeatureMenuEntry = MenuEntryBase & { group: "features"; key: keyof FeatureSettings; cycleValues?: never; cycleValueLabels?: never; cycleEnabledBy?: never; cycleDisabledValue?: never };
|
|
179
183
|
type MenuEntry = DecorationMenuEntry | FeatureMenuEntry;
|
|
184
|
+
const THINKING_LEVEL_CYCLE_LABELS = { "thinking-level": "thinkingLevel" } as const;
|
|
185
|
+
const DIRECTION_CYCLE_LABELS = { ltr: "Left to Right", rtl: "Right to Left" } as const;
|
|
186
|
+
const SPEED_CYCLE_LABELS = { slow: "Slow", normal: "Normal", fast: "Fast" } as const;
|
|
180
187
|
export const MENU_ENTRIES: readonly MenuEntry[] = [
|
|
181
188
|
{ id: "decorateUserPrompt", label: "High-vis prompt", section: "User Prompt", group: "decorations", key: "decorateUserPrompt" },
|
|
182
189
|
{ id: "borderStyle", label: "Border style", section: "User Prompt", group: "decorations", key: "borderStyle", cycleValues: BORDER_STYLE_VALUES, cycleEnabledBy: "borderStyleEnabled", cycleDisabledValue: "double" },
|
|
183
|
-
{ id: "borderColor", label: "Border color", section: "User Prompt", group: "decorations", key: "borderColor", cycleValues:
|
|
190
|
+
{ id: "borderColor", label: "Border color", section: "User Prompt", group: "decorations", key: "borderColor", cycleValues: PROMPT_BORDER_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS, cycleEnabledBy: "borderColorEnabled", cycleDisabledValue: "thinking-level" },
|
|
184
191
|
{ id: "promptIcon", label: "Pi icon", section: "User Prompt", group: "decorations", key: "promptIcon" },
|
|
185
192
|
{ id: "promptTimestamp", label: "Timestamp", section: "User Prompt", group: "decorations", key: "promptTimestamp" },
|
|
186
193
|
{ id: "promptProvider", label: "Provider", section: "User Prompt", group: "decorations", key: "promptProvider" },
|
|
187
194
|
{ id: "promptModel", label: "Model", section: "User Prompt", group: "decorations", key: "promptModel" },
|
|
188
195
|
{ id: "animatedSpinner", label: "Animated spinner", section: "“Working” Loader", group: "decorations", key: "animatedSpinner" },
|
|
189
|
-
{ id: "spinnerColor", label: "Animated spinner color", section: "“Working” Loader", group: "decorations", key: "spinnerColor", cycleValues:
|
|
196
|
+
{ id: "spinnerColor", label: "Animated spinner color", section: "“Working” Loader", group: "decorations", key: "spinnerColor", cycleValues: SPINNER_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS, cycleEnabledBy: "spinnerColorEnabled", cycleDisabledValue: "thinking-level" },
|
|
190
197
|
{ id: "substituteDefaultMessage", label: "Randomize “Working” text", section: "“Working” Loader", group: "features", key: "substituteDefaultMessage" },
|
|
191
198
|
{ id: "shimmer", label: "Text shimmer", section: "“Working” Loader", group: "decorations", key: "shimmer" },
|
|
192
199
|
{ id: "shimmerInverted", label: "Invert shimmer", section: "“Working” Loader", group: "decorations", key: "shimmerInverted" },
|
|
193
|
-
{ id: "shimmerDirection", label: "Text shimmer direction", section: "“Working” Loader", group: "decorations", key: "shimmerDirection", cycleValues: ["
|
|
194
|
-
{ id: "shimmerSpeed", label: "Text shimmer speed", section: "“Working” Loader", group: "decorations", key: "shimmerSpeed", cycleValues: ["
|
|
200
|
+
{ id: "shimmerDirection", label: "Text shimmer direction", section: "“Working” Loader", group: "decorations", key: "shimmerDirection", cycleValues: ["ltr", "rtl"], cycleValueLabels: DIRECTION_CYCLE_LABELS, cycleEnabledBy: "shimmerDirectionEnabled", cycleDisabledValue: "ltr" },
|
|
201
|
+
{ id: "shimmerSpeed", label: "Text shimmer speed", section: "“Working” Loader", group: "decorations", key: "shimmerSpeed", cycleValues: ["slow", "normal", "fast"], cycleValueLabels: SPEED_CYCLE_LABELS, cycleEnabledBy: "shimmerSpeedEnabled", cycleDisabledValue: "normal" },
|
|
195
202
|
{ id: "tokenActivityMonitor", label: "Token activity monitor", section: "“Working” Loader", group: "decorations", key: "tokenActivityMonitor" },
|
|
196
|
-
{ id: "meterColor", label: "Token activity monitor color", section: "“Working” Loader", group: "decorations", key: "meterColor", cycleValues:
|
|
197
|
-
{ id: "meterDirection", label: "Token activity monitor direction", section: "“Working” Loader", group: "decorations", key: "meterDirection", cycleValues: ["
|
|
203
|
+
{ id: "meterColor", label: "Token activity monitor color", section: "“Working” Loader", group: "decorations", key: "meterColor", cycleValues: THINKING_LEVEL_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS, cycleEnabledBy: "meterColorEnabled", cycleDisabledValue: "accent" },
|
|
204
|
+
{ id: "meterDirection", label: "Token activity monitor direction", section: "“Working” Loader", group: "decorations", key: "meterDirection", cycleValues: ["ltr", "rtl"], cycleValueLabels: DIRECTION_CYCLE_LABELS, cycleEnabledBy: "meterDirectionEnabled", cycleDisabledValue: "rtl" },
|
|
198
205
|
{ id: "meterDimmed", label: "Token activity monitor dimmed", section: "“Working” Loader", group: "decorations", key: "meterDimmed" },
|
|
199
206
|
{ id: "elapsedTime", label: "Elapsed time since prompt", section: "“Working” Loader", group: "features", key: "elapsedTime" },
|
|
200
207
|
{ id: "outputTokens", label: "Show output tokens", section: "“Working” Loader", group: "features", key: "outputTokens" },
|
|
201
208
|
// id differs from key: the Elements Order row already owns "tokenRate" in the menu's shared value namespace.
|
|
202
209
|
{ id: "showTokenRate", label: "Token rate", section: "“Working” Loader", group: "features", key: "tokenRate" },
|
|
203
|
-
{ id: "tokenRateColor", label: "Token rate color", section: "“Working” Loader", group: "decorations", key: "tokenRateColor", cycleValues:
|
|
210
|
+
{ id: "tokenRateColor", label: "Token rate color", section: "“Working” Loader", group: "decorations", key: "tokenRateColor", cycleValues: THINKING_LEVEL_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS },
|
|
204
211
|
{ id: "tokenRateDimmed", label: "Token rate dimmed", section: "“Working” Loader", group: "decorations", key: "tokenRateDimmed" },
|
|
205
212
|
// id differs from key: the Elements Order row already owns "responseModel" in the menu's shared value namespace.
|
|
206
213
|
{ id: "showResponseModel", label: "Response model", section: "“Working” Loader", group: "features", key: "responseModel" },
|
|
207
|
-
{ id: "responseModelColor", label: "Response model color", section: "“Working” Loader", group: "decorations", key: "responseModelColor", cycleValues:
|
|
214
|
+
{ id: "responseModelColor", label: "Response model color", section: "“Working” Loader", group: "decorations", key: "responseModelColor", cycleValues: THINKING_LEVEL_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS },
|
|
208
215
|
{ id: "responseModelDimmed", label: "Response model dimmed", section: "“Working” Loader", group: "decorations", key: "responseModelDimmed" },
|
|
209
216
|
{ id: "doneMarker", label: "Show completion marker", section: "Completion Marker", group: "features", key: "doneMarker" },
|
|
210
217
|
{ id: "doneMarkerStyle", label: "Marker style", section: "Completion Marker", group: "decorations", key: "doneMarkerStyle", cycleValues: DONE_MARKER_STYLE_VALUES },
|
|
211
218
|
{ id: "doneMarkerBorderStyle", label: "Border style", section: "Completion Marker", group: "decorations", key: "doneMarkerBorderStyle", cycleValues: DONE_MARKER_BORDER_STYLE_VALUES },
|
|
212
|
-
{ id: "doneMarkerBorderColor", label: "Border color", section: "Completion Marker", group: "decorations", key: "doneMarkerBorderColor", cycleValues:
|
|
219
|
+
{ id: "doneMarkerBorderColor", label: "Border color", section: "Completion Marker", group: "decorations", key: "doneMarkerBorderColor", cycleValues: DONE_MARKER_BORDER_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS },
|
|
213
220
|
{ id: "doneMarkerIcon", label: "Pi icon", section: "Completion Marker", group: "features", key: "doneMarkerIcon" },
|
|
214
221
|
{ id: "randomizeDoneMarker", label: "Randomize “Worked” text", section: "Completion Marker", group: "features", key: "randomizeDoneMarker" },
|
|
215
222
|
{ id: "doneMarkerTokens", label: "Tokens spent", section: "Completion Marker", group: "features", key: "doneMarkerTokens" },
|
|
@@ -222,7 +229,7 @@ function menuItem(entry: MenuEntry, settings: DecoratorSettings): MenuSection["i
|
|
|
222
229
|
const cycleEnabled = entry.group === "decorations" && entry.cycleEnabledBy
|
|
223
230
|
? settings.decorations[entry.cycleEnabledBy]
|
|
224
231
|
: undefined;
|
|
225
|
-
return { id: entry.id, label: entry.label, cycleValues: entry.cycleValues, cycleEnabledBy: entry.cycleEnabledBy, cycleDisabledValue: entry.cycleDisabledValue, cycleEnabled, value
|
|
232
|
+
return { id: entry.id, label: entry.label, cycleValues: entry.cycleValues, cycleValueLabels: entry.cycleValueLabels, cycleEnabledBy: entry.cycleEnabledBy, cycleDisabledValue: entry.cycleDisabledValue, cycleEnabled, value };
|
|
226
233
|
}
|
|
227
234
|
|
|
228
235
|
function isDecorationBooleanKey(key: keyof DecorationSettings): key is DecorationBooleanKey {
|
|
@@ -230,31 +237,36 @@ function isDecorationBooleanKey(key: keyof DecorationSettings): key is Decoratio
|
|
|
230
237
|
}
|
|
231
238
|
|
|
232
239
|
function setDecorationCycleValue(decorations: DecorationSettings, key: keyof DecorationSettings, value: string): void {
|
|
233
|
-
const stored = fromCycleValue(value);
|
|
234
240
|
switch (key) {
|
|
235
241
|
case "borderColor":
|
|
242
|
+
if (isPromptBorderColor(value)) decorations[key] = value;
|
|
243
|
+
return;
|
|
236
244
|
case "spinnerColor":
|
|
245
|
+
if (isSpinnerColor(value)) decorations[key] = value;
|
|
246
|
+
return;
|
|
237
247
|
case "meterColor":
|
|
238
248
|
case "tokenRateColor":
|
|
239
249
|
case "responseModelColor":
|
|
250
|
+
if (isThinkingLevelColor(value)) decorations[key] = value;
|
|
251
|
+
return;
|
|
240
252
|
case "doneMarkerBorderColor":
|
|
241
|
-
if (
|
|
253
|
+
if (isDoneMarkerBorderColor(value)) decorations[key] = value;
|
|
242
254
|
return;
|
|
243
255
|
case "borderStyle":
|
|
244
|
-
if (isBorderStyle(
|
|
256
|
+
if (isBorderStyle(value)) decorations[key] = value;
|
|
245
257
|
return;
|
|
246
258
|
case "doneMarkerBorderStyle":
|
|
247
|
-
if (isDoneMarkerBorderStyle(
|
|
259
|
+
if (isDoneMarkerBorderStyle(value)) decorations[key] = value;
|
|
248
260
|
return;
|
|
249
261
|
case "doneMarkerStyle":
|
|
250
|
-
if (isDoneMarkerStyle(
|
|
262
|
+
if (isDoneMarkerStyle(value)) decorations[key] = value;
|
|
251
263
|
return;
|
|
252
264
|
case "shimmerDirection":
|
|
253
265
|
case "meterDirection":
|
|
254
|
-
if (
|
|
266
|
+
if (value === "ltr" || value === "rtl") decorations[key] = value;
|
|
255
267
|
return;
|
|
256
268
|
case "shimmerSpeed":
|
|
257
|
-
if (
|
|
269
|
+
if (value === "slow" || value === "normal" || value === "fast") decorations[key] = value;
|
|
258
270
|
return;
|
|
259
271
|
default:
|
|
260
272
|
// Fail loudly if a MENU_ENTRIES cycle entry is added without a handler here.
|
|
@@ -306,6 +318,10 @@ export function applyMenuResult(settings: DecoratorSettings, values: Record<stri
|
|
|
306
318
|
* to defaults on any read/parse error. When a cycle setting's enabled flag is
|
|
307
319
|
* off, its stored value resets to the menu's disabled default so stale choices
|
|
308
320
|
* don't resurface on re-enable.
|
|
321
|
+
* Files with `schemaVersion` below 2 get spinner color reset to `thinking-level` and
|
|
322
|
+
* `borderAccent` prompt borders moved to `thinking-level`; below 3, the completion-marker
|
|
323
|
+
* border color is reset. A legacy `features.simCityWorkingText: true` enables the
|
|
324
|
+
* `simcity` pack when no explicit preference is stored.
|
|
309
325
|
*/
|
|
310
326
|
export function loadSettings(): DecoratorSettings {
|
|
311
327
|
try {
|
|
@@ -321,6 +337,14 @@ export function loadSettings(): DecoratorSettings {
|
|
|
321
337
|
wordPacks.simcity = true;
|
|
322
338
|
}
|
|
323
339
|
const settings = { decorations: mergeGroup(DEFAULT_SETTINGS.decorations, parsed.decorations), features: mergeGroup(DEFAULT_SETTINGS.features, parsed.features), loaderOrder: parseLoaderOrder(parsed.loaderOrder), wordPacks };
|
|
340
|
+
const schemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 1;
|
|
341
|
+
if (schemaVersion < THINKING_LEVEL_COLOR_MIGRATION_VERSION) {
|
|
342
|
+
settings.decorations.spinnerColor = "thinking-level";
|
|
343
|
+
if (settings.decorations.borderColor === "borderAccent") settings.decorations.borderColor = "thinking-level";
|
|
344
|
+
}
|
|
345
|
+
if (schemaVersion < DONE_MARKER_BORDER_COLOR_MIGRATION_VERSION) {
|
|
346
|
+
settings.decorations.doneMarkerBorderColor = "thinking-level";
|
|
347
|
+
}
|
|
324
348
|
for (const entry of MENU_ENTRIES) {
|
|
325
349
|
if (entry.group === "decorations" && entry.cycleEnabledBy && entry.cycleDisabledValue !== undefined && !settings.decorations[entry.cycleEnabledBy]) {
|
|
326
350
|
setDecorationCycleValue(settings.decorations, entry.key, entry.cycleDisabledValue);
|
|
@@ -343,5 +367,5 @@ export function atomicWriteFile(path: string, contents: string): void {
|
|
|
343
367
|
}
|
|
344
368
|
|
|
345
369
|
export function saveSettings(settings: DecoratorSettings): void {
|
|
346
|
-
atomicWriteFile(settingsPath(), `${JSON.stringify(settings, null, 2)}\n`);
|
|
370
|
+
atomicWriteFile(settingsPath(), `${JSON.stringify({ schemaVersion: SETTINGS_SCHEMA_VERSION, ...settings }, null, 2)}\n`);
|
|
347
371
|
}
|