@underactive/pi-topping 0.6.5 → 0.7.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 +7 -4
- package/package.json +1 -1
- package/src/activity-meter.ts +3 -3
- package/src/format.ts +30 -10
- package/src/menu.ts +21 -34
- package/src/preview.ts +18 -17
- package/src/prompt-decorator.ts +27 -16
- package/src/session.ts +50 -45
- package/src/settings.ts +75 -55
- package/src/word-packs.ts +1 -1
- package/src/words.ts +2 -10
package/README.md
CHANGED
|
@@ -31,12 +31,12 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
31
31
|
╔═[ Pi Topping: Settings ]══════════════════════════════════════╗
|
|
32
32
|
╟─ Preview ─────────────────────────────────────────────────────╢
|
|
33
33
|
║ ║
|
|
34
|
-
║ ⠋ Crafting
|
|
34
|
+
║ ⠋ Crafting ⣠⣤⣶⣶⣤⣠⣀⢀ 28 tps · 3s · ↓ 84 tokens · test-model ║
|
|
35
35
|
║ ║
|
|
36
36
|
╟─ User Prompt ─────────────────────────────────────────────────╢
|
|
37
37
|
║ [■] High-vis prompt ON ║
|
|
38
38
|
║ [■] Border style ‹ double › ║
|
|
39
|
-
║ [■] Border color
|
|
39
|
+
║ [■] Border color ‹ thinkingLevel › ║
|
|
40
40
|
║ [■] Pi icon ON ║
|
|
41
41
|
║ [■] Timestamp ON ║
|
|
42
42
|
║ [■] Provider ON ║
|
|
@@ -44,7 +44,7 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
44
44
|
║ ║
|
|
45
45
|
╟─ “Working” Loader ────────────────────────────────────────────╢
|
|
46
46
|
║ [■] Animated spinner ON ║
|
|
47
|
-
║ ❯ [■] Animated spinner color
|
|
47
|
+
║ ❯ [■] Animated spinner color ‹ thinkingLevel › ║
|
|
48
48
|
║ [■] Randomize “Working” text ON ║
|
|
49
49
|
║ [■] Text shimmer ON ║
|
|
50
50
|
║ [■] Invert shimmer OFF ║
|
|
@@ -76,7 +76,7 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
76
76
|
║ [■] Show completion marker ON ║
|
|
77
77
|
║ [■] Marker style ‹ elite › ║
|
|
78
78
|
║ [■] Border style ‹ none › ║
|
|
79
|
-
║ [■] Border color
|
|
79
|
+
║ [■] Border color ‹ thinkingLevel › ║
|
|
80
80
|
║ [■] Pi icon ON ║
|
|
81
81
|
║ [■] Randomize “Worked” text ON ║
|
|
82
82
|
║ [■] Tokens spent ON ║
|
|
@@ -119,6 +119,8 @@ Bundled packs live in [`wordpacks/`](wordpacks/). Copy `wordpacks/doctor-who.jso
|
|
|
119
119
|
|
|
120
120
|
Pack IDs must start with a lowercase letter and contain only lowercase letters, digits, and `-`. Each pack needs a non-empty `name` and at least one word with non-empty `present_tense` and `past_tense` strings. Invalid entries are ignored. Packs reload when a session starts and whenever `/topping-settings` opens, not while a turn is running. Preferences for missing packs are retained so they apply when the pack returns.
|
|
121
121
|
|
|
122
|
+
The `default` animated spinner and completion marker border colors defer to Pi's thinking-level color. The `thinking-level` prompt border color follows the level captured when the prompt is submitted. The `thinkingLevel` color option for the token activity monitor, token rate, and response model follows Pi's active thinking-level color.
|
|
123
|
+
|
|
122
124
|
Under **Elements Order**, press `␣` to grab a row, then `↑`/`↓` to slide that element
|
|
123
125
|
left or right within the loader. Elapsed time, output tokens, token rate, and response model are joined with
|
|
124
126
|
`·` separators whenever adjacent. When reordering separates them, each adjacent run is rendered
|
|
@@ -136,6 +138,7 @@ The response model is captured from assistant responses, displayed as its saniti
|
|
|
136
138
|
## Requirements
|
|
137
139
|
|
|
138
140
|
- [Pi](https://github.com/earendil-works/pi-coding-agent). Pi ≥0.84.4 is required for the waiting-for-input loader state; older Pi versions keep the animated loader.
|
|
141
|
+
- On Pi ≥0.85.0, the working indicator is embedded in the default editor border and long loaders are truncated to the border width instead of wrapping; custom editors that do not opt in keep the standalone wrapping indicator.
|
|
139
142
|
- Node.js ≥22.19.0
|
|
140
143
|
|
|
141
144
|
## Development
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@underactive/pi-topping",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "A Pi extension that adds quality of life improvements to Pi's UX: a high-vis user prompt, a shimmering working loader with token activity meter and elapsed timer, and an end-of-turn completion marker",
|
|
5
5
|
"keywords": ["pi-package", "pi-extension"],
|
|
6
6
|
"license": "MIT",
|
package/src/activity-meter.ts
CHANGED
|
@@ -123,16 +123,16 @@ export class ActivityMeter {
|
|
|
123
123
|
this.#levels.fill(0);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
-
/** Colorize a meter cell
|
|
126
|
+
/** Colorize a meter cell (dim at IDLE, `colorize` otherwise). */
|
|
127
127
|
static colorizeCell(
|
|
128
128
|
level: ActivityMeterLevel,
|
|
129
129
|
char: string,
|
|
130
130
|
theme: { fg: (style: ThemeColor, s: string) => string },
|
|
131
|
-
|
|
131
|
+
colorize: (text: string) => string,
|
|
132
132
|
dimmed?: boolean,
|
|
133
133
|
): string {
|
|
134
134
|
if (level === ActivityMeterLevel.IDLE) return theme.fg("dim", char);
|
|
135
|
-
const colored =
|
|
135
|
+
const colored = colorize(char);
|
|
136
136
|
return dimmed ? dimAttribute(colored) : colored;
|
|
137
137
|
}
|
|
138
138
|
}
|
package/src/format.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Theme
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ThinkingLevelColor, ThinkingLevelSettingColor } from "./settings.ts";
|
|
2
3
|
|
|
3
4
|
/** Pi's default working-indicator frames (same braille spinner as pi-tui's Loader). */
|
|
4
5
|
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -6,8 +7,27 @@ export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦",
|
|
|
6
7
|
/** Milliseconds per spinner frame, matching pi-tui's Loader cadence. */
|
|
7
8
|
export const SPINNER_FRAME_MS = 80;
|
|
8
9
|
|
|
9
|
-
/** Default "Working
|
|
10
|
-
export const DEFAULT_WORKING_WORD = "Working
|
|
10
|
+
/** Default "Working" text shown when text randomization/substitution is off. */
|
|
11
|
+
export const DEFAULT_WORKING_WORD = "Working";
|
|
12
|
+
|
|
13
|
+
export type ThinkingLevel = Parameters<Theme["getThinkingBorderColor"]>[0];
|
|
14
|
+
|
|
15
|
+
export const THINKING_LEVEL_VALUES = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
|
|
16
|
+
|
|
17
|
+
export function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
18
|
+
return typeof value === "string" && THINKING_LEVEL_VALUES.some(level => level === value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Build a colorizer for a configured color, including Pi's active thinking-level color. */
|
|
22
|
+
export function getThinkingLevelColorizer(
|
|
23
|
+
theme: Pick<Theme, "fg" | "getThinkingBorderColor">,
|
|
24
|
+
color: ThinkingLevelColor | ThinkingLevelSettingColor,
|
|
25
|
+
thinkingLevel: ThinkingLevel | undefined,
|
|
26
|
+
): (text: string) => string {
|
|
27
|
+
return color === "thinking-level"
|
|
28
|
+
? theme.getThinkingBorderColor(thinkingLevel ?? "off")
|
|
29
|
+
: (text: string) => theme.fg(color, text);
|
|
30
|
+
}
|
|
11
31
|
|
|
12
32
|
/** Milliseconds between activity meter sample pushes. */
|
|
13
33
|
export const METER_INTERVAL_MS = 100;
|
|
@@ -24,7 +44,7 @@ export type LoaderElement = "spinner" | "text" | "meter" | "elapsed" | "tokens"
|
|
|
24
44
|
/** Default first-use working-indicator element order. */
|
|
25
45
|
export const DEFAULT_LOADER_ORDER: readonly LoaderElement[] = ["spinner", "text", "meter", "tokenRate", "elapsed", "tokens", "responseModel"];
|
|
26
46
|
|
|
27
|
-
/** Elements share a detail group; separators and non-rate details
|
|
47
|
+
/** Elements share a detail group; separators are dimmed and non-rate details use muted. */
|
|
28
48
|
const DETAIL_ELEMENTS: ReadonlySet<LoaderElement> = new Set(["elapsed", "tokens", "tokenRate", "responseModel"]);
|
|
29
49
|
|
|
30
50
|
const TOKEN_UNITS = [
|
|
@@ -84,13 +104,13 @@ export const RESPONSE_MODEL_FADE_MS = 500;
|
|
|
84
104
|
export function fadeThemeColorString(
|
|
85
105
|
text: string,
|
|
86
106
|
shade: number,
|
|
87
|
-
theme: Pick<Theme, "getFgAnsi"
|
|
88
|
-
|
|
107
|
+
theme: Pick<Theme, "getFgAnsi">,
|
|
108
|
+
colorizer: (text: string) => string,
|
|
89
109
|
): string {
|
|
90
110
|
if (!text) return "";
|
|
91
|
-
const source = ansiToRgb(
|
|
111
|
+
const source = ansiToRgb(colorizer("x"));
|
|
92
112
|
const dim = ansiToRgb(theme.getFgAnsi("dim"));
|
|
93
|
-
if (!source || !dim) return
|
|
113
|
+
if (!source || !dim) return colorizer(text);
|
|
94
114
|
const clampedShade = Math.max(0, Math.min(TOKEN_RATE_FADE_SHADE_COUNT - 1, Math.floor(shade)));
|
|
95
115
|
const progress = (clampedShade + 1) / TOKEN_RATE_FADE_SHADE_COUNT;
|
|
96
116
|
const eased = 0.5 * (1 - Math.cos(Math.PI * progress));
|
|
@@ -151,7 +171,7 @@ export function buildWorkingMessage(
|
|
|
151
171
|
if (!value) continue;
|
|
152
172
|
if (DETAIL_ELEMENTS.has(element)) {
|
|
153
173
|
// tokenRate and responseModel arrive pre-colored; dimming them here would replace their configured colors.
|
|
154
|
-
details.push(element === "tokenRate" || element === "responseModel" ? value : theme.fg("
|
|
174
|
+
details.push(element === "tokenRate" || element === "responseModel" ? value : theme.fg("muted", value));
|
|
155
175
|
continue;
|
|
156
176
|
}
|
|
157
177
|
flushDetails();
|
|
@@ -220,7 +240,7 @@ export function shimmerString(
|
|
|
220
240
|
}
|
|
221
241
|
|
|
222
242
|
function ansiToRgb(ansi: string): [number, number, number] | null {
|
|
223
|
-
const match = ansi.match(
|
|
243
|
+
const match = ansi.match(/\x1b\[38;2;(\d+);(\d+);(\d+)m/);
|
|
224
244
|
if (!match) return null;
|
|
225
245
|
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
226
246
|
}
|
package/src/menu.ts
CHANGED
|
@@ -28,6 +28,8 @@ export interface MenuItem {
|
|
|
28
28
|
value: MenuValue;
|
|
29
29
|
/** Values cycled with left/right arrows. Omit for a boolean space-toggle. */
|
|
30
30
|
cycleValues?: readonly string[];
|
|
31
|
+
/** Optional display labels for cycle values; the keys remain the published values. */
|
|
32
|
+
cycleValueLabels?: Readonly<Record<string, string>>;
|
|
31
33
|
/** ID of the boolean value that gates cycling; space toggles it. */
|
|
32
34
|
cycleEnabledBy?: string;
|
|
33
35
|
/** Initial enabled state when `cycleEnabledBy` is set (default true). */
|
|
@@ -59,11 +61,9 @@ export interface MenuConfig {
|
|
|
59
61
|
* since the menu opened. Lines may contain ANSI styling and are truncated/
|
|
60
62
|
* padded to fit automatically.
|
|
61
63
|
*
|
|
62
|
-
* Return `string[]` for legacy interval-based animation (defaults to 50ms
|
|
63
|
-
* refresh), or a `PreviewResult` to declare the next refresh interval.
|
|
64
64
|
* Omitting `nextRefreshInMs` from a `PreviewResult` makes the preview static.
|
|
65
65
|
*/
|
|
66
|
-
preview?: (values: Record<string, MenuValue>, elapsedMs: number, activeItemId: string | undefined, width: number) =>
|
|
66
|
+
preview?: (values: Record<string, MenuValue>, elapsedMs: number, activeItemId: string | undefined, width: number) => PreviewResult;
|
|
67
67
|
/** Optional heading for the preview block. Defaults to `Preview`. */
|
|
68
68
|
previewTitle?: string;
|
|
69
69
|
/** Maximum overlay height in rows or as a percentage of the terminal height. */
|
|
@@ -88,12 +88,6 @@ function closeTruncatedHyperlink(text: string): string {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
interface FlatItem {
|
|
91
|
-
id: string;
|
|
92
|
-
label: string;
|
|
93
|
-
cycleValues?: readonly string[];
|
|
94
|
-
cycleEnabledBy?: string;
|
|
95
|
-
cycleDisabledValue?: string;
|
|
96
|
-
reorderGroup?: string;
|
|
97
91
|
item: MenuItem;
|
|
98
92
|
sectionIndex: number;
|
|
99
93
|
}
|
|
@@ -159,7 +153,7 @@ export class MenuComponent implements Component {
|
|
|
159
153
|
this.flat = [];
|
|
160
154
|
for (const [sectionIndex, section] of config.sections.entries()) {
|
|
161
155
|
for (const item of section.items) {
|
|
162
|
-
this.flat.push({
|
|
156
|
+
this.flat.push({ item, sectionIndex });
|
|
163
157
|
}
|
|
164
158
|
}
|
|
165
159
|
this.initialValues = { ...this.values };
|
|
@@ -219,7 +213,7 @@ export class MenuComponent implements Component {
|
|
|
219
213
|
[Key.left]: () => this.cycleCurrentValue(-1),
|
|
220
214
|
[Key.right]: () => this.cycleCurrentValue(1),
|
|
221
215
|
[Key.space]: () => {
|
|
222
|
-
const item = this.flat[this.cursor]
|
|
216
|
+
const item = this.flat[this.cursor]!.item;
|
|
223
217
|
if (item.cycleValues && item.cycleEnabledBy) {
|
|
224
218
|
this.values[item.cycleEnabledBy] = !this.values[item.cycleEnabledBy] as boolean;
|
|
225
219
|
if (!this.values[item.cycleEnabledBy] && item.cycleDisabledValue !== undefined) this.values[item.id] = item.cycleDisabledValue;
|
|
@@ -258,20 +252,20 @@ export class MenuComponent implements Component {
|
|
|
258
252
|
*/
|
|
259
253
|
private moveGrabbedItem(delta: number): boolean {
|
|
260
254
|
const grabbed = this.flat[this.cursor]!;
|
|
261
|
-
const group = grabbed.reorderGroup;
|
|
262
|
-
if (group === undefined || this.values[grabbed.id] !== true) return false;
|
|
255
|
+
const group = grabbed.item.reorderGroup;
|
|
256
|
+
if (group === undefined || this.values[grabbed.item.id] !== true) return false;
|
|
263
257
|
|
|
264
258
|
let start = this.cursor;
|
|
265
|
-
while (start > 0 && this.flat[start - 1]!.reorderGroup === group) start--;
|
|
259
|
+
while (start > 0 && this.flat[start - 1]!.item.reorderGroup === group) start--;
|
|
266
260
|
let end = this.cursor;
|
|
267
|
-
while (end < this.flat.length - 1 && this.flat[end + 1]!.reorderGroup === group) end++;
|
|
261
|
+
while (end < this.flat.length - 1 && this.flat[end + 1]!.item.reorderGroup === group) end++;
|
|
268
262
|
|
|
269
263
|
const target = Math.max(start, Math.min(end, this.cursor + delta));
|
|
270
264
|
if (target !== this.cursor) {
|
|
271
265
|
this.flat.splice(this.cursor, 1);
|
|
272
266
|
this.flat.splice(target, 0, grabbed);
|
|
273
267
|
this.cursor = target;
|
|
274
|
-
this.values[group] = this.flat.slice(start, end + 1).map(flat => flat.id).join(",");
|
|
268
|
+
this.values[group] = this.flat.slice(start, end + 1).map(flat => flat.item.id).join(",");
|
|
275
269
|
this.invalidate();
|
|
276
270
|
}
|
|
277
271
|
return true;
|
|
@@ -292,7 +286,7 @@ export class MenuComponent implements Component {
|
|
|
292
286
|
}
|
|
293
287
|
|
|
294
288
|
private cycleCurrentValue(delta: number): void {
|
|
295
|
-
const item = this.flat[this.cursor]
|
|
289
|
+
const item = this.flat[this.cursor]!.item;
|
|
296
290
|
if (!item.cycleValues?.length || (item.cycleEnabledBy && !this.values[item.cycleEnabledBy])) return;
|
|
297
291
|
const current = item.cycleValues.indexOf(this.values[item.id] as string);
|
|
298
292
|
const index = (current + delta + item.cycleValues.length) % item.cycleValues.length;
|
|
@@ -306,14 +300,9 @@ export class MenuComponent implements Component {
|
|
|
306
300
|
const result = this.previewFn(
|
|
307
301
|
this.values,
|
|
308
302
|
this.previewOrigin !== undefined ? Date.now() - this.previewOrigin : 0,
|
|
309
|
-
this.flat[this.cursor]?.id,
|
|
303
|
+
this.flat[this.cursor]?.item.id,
|
|
310
304
|
width,
|
|
311
305
|
);
|
|
312
|
-
if (Array.isArray(result)) {
|
|
313
|
-
this.previewNextRefreshMs = 50;
|
|
314
|
-
return result;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
306
|
this.previewNextRefreshMs = result.nextRefreshInMs;
|
|
318
307
|
return result.lines;
|
|
319
308
|
}
|
|
@@ -382,12 +371,7 @@ export class MenuComponent implements Component {
|
|
|
382
371
|
}
|
|
383
372
|
|
|
384
373
|
/** Render the scrolling settings body while keeping the selected item in view. */
|
|
385
|
-
private buildResponsiveToggleSections(innerWidth: number, maxRows: number
|
|
386
|
-
if (allLines.length <= maxRows) {
|
|
387
|
-
this.scrollStart = 0;
|
|
388
|
-
this.pageItemCount = Math.max(1, this.flat.length);
|
|
389
|
-
return allLines;
|
|
390
|
-
}
|
|
374
|
+
private buildResponsiveToggleSections(innerWidth: number, maxRows: number): string[] {
|
|
391
375
|
if (maxRows <= 0) {
|
|
392
376
|
this.pageItemCount = 1;
|
|
393
377
|
return [];
|
|
@@ -452,15 +436,16 @@ export class MenuComponent implements Component {
|
|
|
452
436
|
const previewLines = this.samplePreview(Math.max(0, innerWidth - 1));
|
|
453
437
|
const header = [this.renderTopBorder(innerWidth), ...this.buildPreviewBlock(previewLines, innerWidth)];
|
|
454
438
|
const footer = this.buildFooter(innerWidth);
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
if (maxRows === undefined ||
|
|
439
|
+
const sectionsWithItems = new Set(this.flat.map(f => f.sectionIndex)).size;
|
|
440
|
+
const naturalBodyLength = this.flat.length + 2 * sectionsWithItems;
|
|
441
|
+
if (maxRows === undefined || header.length + naturalBodyLength + footer.length <= maxRows) {
|
|
442
|
+
const naturalBody = this.buildToggleSections(innerWidth);
|
|
458
443
|
this.pageItemCount = Math.max(1, this.flat.length);
|
|
459
444
|
return [...header, ...naturalBody, ...footer].map(line => truncateToWidth(line, boxWidth, ""));
|
|
460
445
|
}
|
|
461
446
|
|
|
462
447
|
const bodyRows = Math.max(0, maxRows - header.length - footer.length);
|
|
463
|
-
const body = this.buildResponsiveToggleSections(innerWidth, bodyRows
|
|
448
|
+
const body = this.buildResponsiveToggleSections(innerWidth, bodyRows);
|
|
464
449
|
return [...header, ...body, ...footer].slice(0, maxRows).map(line => truncateToWidth(line, boxWidth, ""));
|
|
465
450
|
}
|
|
466
451
|
|
|
@@ -557,7 +542,9 @@ export class MenuComponent implements Component {
|
|
|
557
542
|
|
|
558
543
|
if (item.cycleValues) {
|
|
559
544
|
const enabled = item.cycleEnabledBy ? this.values[item.cycleEnabledBy] as boolean : true;
|
|
560
|
-
const
|
|
545
|
+
const rawValue = typeof value === "string" ? value : String(value);
|
|
546
|
+
const displayValue = item.cycleValueLabels?.[rawValue] ?? rawValue;
|
|
547
|
+
const stateWord = `‹ ${displayValue} ›`;
|
|
561
548
|
const maxLabelLen = Math.max(0, innerWidth - ROW_PREFIX_WIDTH - visibleWidth(stateWord) - 1);
|
|
562
549
|
const label = visibleWidth(item.label) > maxLabelLen ? truncateToWidth(item.label, maxLabelLen) : item.label;
|
|
563
550
|
const leftPlain = ` ${marker} [${enabled ? "■" : " "}] ${label}`;
|
package/src/preview.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { DEFAULT_PREVIEW_WIDTH, type PreviewResult } from "./menu.ts";
|
|
3
3
|
import { ActivityMeter, rateToLevel } from "./activity-meter.ts";
|
|
4
|
-
import { buildWorkingMessage, DEFAULT_WORKING_WORD, dimAttribute, ELAPSED_INTERVAL_MS, formatElapsed, formatTokenRate, formatTokens, isFullyDefaultAppearance, METER_INTERVAL_MS, SHIMMER_INTERVAL_MS, shimmerString, SPINNER_FRAME_MS, SPINNER_FRAMES } from "./format.ts";
|
|
4
|
+
import { buildWorkingMessage, DEFAULT_WORKING_WORD, dimAttribute, ELAPSED_INTERVAL_MS, formatElapsed, formatTokenRate, formatTokens, getThinkingLevelColorizer, isFullyDefaultAppearance, METER_INTERVAL_MS, SHIMMER_INTERVAL_MS, shimmerString, SPINNER_FRAME_MS, SPINNER_FRAMES } from "./format.ts";
|
|
5
5
|
import { buildCompletionMarkerContent, buildCompletionMarkerLine, buildPromptBoxLines } from "./prompt-decorator.ts";
|
|
6
|
-
import { DEFAULT_SETTINGS, fromCycleDirection, fromCycleSpeed, isBorderStyle, isDoneMarkerBorderStyle, isDoneMarkerStyle,
|
|
6
|
+
import { DEFAULT_SETTINGS, fromCycleDirection, fromCycleSpeed, isBorderStyle, isDoneMarkerBorderColor, isDoneMarkerBorderStyle, isDoneMarkerStyle, isPromptBorderColor, isSpinnerColor, isThinkingLevelColor, LOADER_ORDER_ID, MENU_ENTRIES, parseLoaderOrder } from "./settings.ts";
|
|
7
7
|
import { isWordPackEnabled, selectWorkingTextSelection, wordPacksPath, type WordPack } from "./word-packs.ts";
|
|
8
|
-
import { pickRandomWord } from "./words.ts";
|
|
9
8
|
// Simulated load for the menu preview: a 2.4s cosine wave peaking at 46 tps for the meter,
|
|
10
9
|
// flat 28 tps for the token readouts.
|
|
11
10
|
const METER_PERIOD_MS = 2400, METER_PEAK_RATE = 46, TOKEN_RATE_PER_SEC = 28;
|
|
@@ -16,7 +15,6 @@ function meterRate(elapsedMs: number): number { return ((1 - Math.cos((2 * Math.
|
|
|
16
15
|
/** Stateful, per-menu preview renderer that follows the active settings section. */
|
|
17
16
|
export class PreviewRenderer {
|
|
18
17
|
// Preserve constructor-time randomness so rendering never changes the preview phrase.
|
|
19
|
-
readonly #word = pickRandomWord();
|
|
20
18
|
readonly #poolFraction = Math.random();
|
|
21
19
|
readonly #packs: readonly WordPack[];
|
|
22
20
|
readonly #meter = new ActivityMeter();
|
|
@@ -54,17 +52,18 @@ export class PreviewRenderer {
|
|
|
54
52
|
const features = { substituteDefaultMessage: values.substituteDefaultMessage !== false, elapsedTime: values.elapsedTime !== false, outputTokens: values.outputTokens !== false, tokenRate: values.showTokenRate !== false, responseModel: values.showResponseModel !== false };
|
|
55
53
|
const decorations = { shimmer: values.shimmer !== false, shimmerInverted: values.shimmerInverted === true, tokenActivityMonitor: values.tokenActivityMonitor !== false };
|
|
56
54
|
let spinnerColor = DEFAULT_SETTINGS.decorations.spinnerColor;
|
|
57
|
-
if (
|
|
55
|
+
if (isSpinnerColor(values.spinnerColor)) {
|
|
58
56
|
spinnerColor = values.spinnerColor;
|
|
59
57
|
}
|
|
60
58
|
let spinner = "";
|
|
61
59
|
if (values.animatedSpinner !== false) {
|
|
62
|
-
spinner = this.#ctx.ui.theme
|
|
60
|
+
spinner = getThinkingLevelColorizer(this.#ctx.ui.theme, spinnerColor, this.#ctx.thinkingLevel)(SPINNER_FRAMES[Math.floor(elapsedMs / SPINNER_FRAME_MS) % SPINNER_FRAMES.length]!);
|
|
63
61
|
}
|
|
64
62
|
const order = parseLoaderOrder(values[LOADER_ORDER_ID]);
|
|
65
63
|
let responseModelColor = DEFAULT_SETTINGS.decorations.responseModelColor;
|
|
66
|
-
if (
|
|
67
|
-
const
|
|
64
|
+
if (isThinkingLevelColor(values.responseModelColor)) responseModelColor = values.responseModelColor;
|
|
65
|
+
const responseModelColorizer = getThinkingLevelColorizer(this.#ctx.ui.theme, responseModelColor, this.#ctx.thinkingLevel);
|
|
66
|
+
const responseModelColored = features.responseModel ? responseModelColorizer("test-model") : "";
|
|
68
67
|
const responseModel = responseModelColored && values.responseModelDimmed === true ? dimAttribute(responseModelColored) : responseModelColored;
|
|
69
68
|
if (isFullyDefaultAppearance(features, decorations)) {
|
|
70
69
|
return buildWorkingMessage(this.#ctx.ui.theme, { spinner, text: this.#ctx.ui.theme.fg("dim", DEFAULT_WORKING_WORD), responseModel }, order);
|
|
@@ -74,8 +73,7 @@ export class PreviewRenderer {
|
|
|
74
73
|
const packValues = this.packValues(values);
|
|
75
74
|
const signature = JSON.stringify(Object.keys(packValues).sort().map(k => `${k}=${packValues[k]}`));
|
|
76
75
|
if (signature !== this.#cachedPackSignature) {
|
|
77
|
-
|
|
78
|
-
this.#cachedPackWord = enabledPacks.length ? selectWorkingTextSelection(packValues, this.#packs, this.#poolFraction).text : this.#word;
|
|
76
|
+
this.#cachedPackWord = selectWorkingTextSelection(packValues, this.#packs, this.#poolFraction).text;
|
|
79
77
|
this.#cachedPackSignature = signature;
|
|
80
78
|
}
|
|
81
79
|
word = this.#cachedPackWord;
|
|
@@ -87,24 +85,26 @@ export class PreviewRenderer {
|
|
|
87
85
|
styledWord = this.#ctx.ui.theme.fg("text", word);
|
|
88
86
|
}
|
|
89
87
|
let meterColor = DEFAULT_SETTINGS.decorations.meterColor;
|
|
90
|
-
if (
|
|
88
|
+
if (isThinkingLevelColor(values.meterColor)) {
|
|
91
89
|
meterColor = values.meterColor;
|
|
92
90
|
}
|
|
93
91
|
let meter = "";
|
|
94
92
|
if (decorations.tokenActivityMonitor) {
|
|
95
|
-
|
|
93
|
+
const meterColorizer = getThinkingLevelColorizer(this.#ctx.ui.theme, meterColor, this.#ctx.thinkingLevel);
|
|
94
|
+
meter = this.#meter.render((level, char) => ActivityMeter.colorizeCell(level, char, this.#ctx.ui.theme, meterColorizer, values.meterDimmed === true));
|
|
96
95
|
}
|
|
97
96
|
let tokenRateText = "";
|
|
98
97
|
if (features.tokenRate) {
|
|
99
98
|
tokenRateText = formatTokenRate(TOKEN_RATE_PER_SEC);
|
|
100
99
|
}
|
|
101
100
|
let tokenRateColor = DEFAULT_SETTINGS.decorations.tokenRateColor;
|
|
102
|
-
if (
|
|
101
|
+
if (isThinkingLevelColor(values.tokenRateColor)) {
|
|
103
102
|
tokenRateColor = values.tokenRateColor;
|
|
104
103
|
}
|
|
105
104
|
let tokenRateColored = "";
|
|
106
105
|
if (tokenRateText) {
|
|
107
|
-
|
|
106
|
+
const tokenRateColorizer = getThinkingLevelColorizer(this.#ctx.ui.theme, tokenRateColor, this.#ctx.thinkingLevel);
|
|
107
|
+
tokenRateColored = tokenRateColorizer(tokenRateText);
|
|
108
108
|
}
|
|
109
109
|
let tokenRate = tokenRateColored;
|
|
110
110
|
if (tokenRateColored && values.tokenRateDimmed === true) {
|
|
@@ -157,8 +157,9 @@ export class PreviewRenderer {
|
|
|
157
157
|
icon: values.useNerdFont ? "" : "π",
|
|
158
158
|
provider: this.#ctx.model?.provider,
|
|
159
159
|
model: this.#ctx.model?.id,
|
|
160
|
-
borderColor:
|
|
160
|
+
borderColor: isPromptBorderColor(borderColor) ? borderColor : DEFAULT_SETTINGS.decorations.borderColor,
|
|
161
161
|
borderStyle: isBorderStyle(borderStyle) ? borderStyle : "double",
|
|
162
|
+
thinkingLevel: this.#ctx.thinkingLevel,
|
|
162
163
|
});
|
|
163
164
|
return timestamp === undefined
|
|
164
165
|
? { lines }
|
|
@@ -174,9 +175,9 @@ export class PreviewRenderer {
|
|
|
174
175
|
if (values.doneMarkerInputs === true) details.push("2 mid-turn inputs");
|
|
175
176
|
const content = buildCompletionMarkerContent(theme, icon, word, "2m 52s", details);
|
|
176
177
|
const borderStyle = isDoneMarkerBorderStyle(values.doneMarkerBorderStyle) ? values.doneMarkerBorderStyle : "none";
|
|
177
|
-
const borderColor =
|
|
178
|
+
const borderColor = isDoneMarkerBorderColor(values.doneMarkerBorderColor) ? values.doneMarkerBorderColor : DEFAULT_SETTINGS.decorations.doneMarkerBorderColor;
|
|
178
179
|
const markerStyle = isDoneMarkerStyle(values.doneMarkerStyle) ? values.doneMarkerStyle : DEFAULT_SETTINGS.decorations.doneMarkerStyle;
|
|
179
|
-
const marker = buildCompletionMarkerLine(content, width, theme, borderStyle, borderColor, markerStyle);
|
|
180
|
+
const marker = buildCompletionMarkerLine(content, width, theme, borderStyle, borderColor, markerStyle, this.#ctx.thinkingLevel);
|
|
180
181
|
return { lines: ["", marker, ""] };
|
|
181
182
|
}
|
|
182
183
|
}
|
package/src/prompt-decorator.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { MessageRenderer
|
|
1
|
+
import type { MessageRenderer } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import { getThinkingLevelColorizer, isThinkingLevel, type ThinkingLevel } from "./format.ts";
|
|
4
|
+
import { DEFAULT_SETTINGS, isBorderStyle, isPromptBorderColor, type BorderStyle, type DoneMarkerBorderColor, type DoneMarkerBorderStyle, type DoneMarkerStyle, type PromptBorderColor } from "./settings.ts";
|
|
4
5
|
import { stripControlChars } from "./util.ts";
|
|
5
6
|
|
|
6
7
|
export const PROMPT_BOX_TYPE = "pi-topping-prompt";
|
|
@@ -25,11 +26,15 @@ export interface PromptBoxDetails {
|
|
|
25
26
|
icon?: string;
|
|
26
27
|
provider?: string;
|
|
27
28
|
model?: string;
|
|
28
|
-
borderColor?:
|
|
29
|
+
borderColor?: PromptBorderColor;
|
|
29
30
|
borderStyle?: BorderStyle;
|
|
31
|
+
thinkingLevel?: ThinkingLevel;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
type PromptTheme = {
|
|
34
|
+
type PromptTheme = {
|
|
35
|
+
fg(color: string, text: string): string;
|
|
36
|
+
getThinkingBorderColor(level: ThinkingLevel): (text: string) => string;
|
|
37
|
+
};
|
|
33
38
|
|
|
34
39
|
const COMPLETION_MARKER_TRAIL_RUNS = [6, 4, 2, 1] as const;
|
|
35
40
|
|
|
@@ -51,15 +56,16 @@ export function buildCompletionMarkerLine(
|
|
|
51
56
|
width: number,
|
|
52
57
|
theme: PromptTheme,
|
|
53
58
|
borderStyle: DoneMarkerBorderStyle,
|
|
54
|
-
borderColor:
|
|
59
|
+
borderColor: DoneMarkerBorderColor,
|
|
55
60
|
markerStyle: DoneMarkerStyle = DEFAULT_SETTINGS.decorations.doneMarkerStyle,
|
|
61
|
+
thinkingLevel?: ThinkingLevel,
|
|
56
62
|
): string {
|
|
57
63
|
const safeWidth = Math.max(0, Math.floor(width));
|
|
58
64
|
if (safeWidth === 0) return "";
|
|
59
65
|
if (borderStyle === "none") return truncateToWidth(content, safeWidth);
|
|
60
66
|
|
|
61
67
|
const g = BORDER_GLYPHS[borderStyle];
|
|
62
|
-
const border = (
|
|
68
|
+
const border = getThinkingLevelColorizer(theme, borderColor, thinkingLevel);
|
|
63
69
|
const prefix = `${g.bl}${g.h.repeat(2)} `;
|
|
64
70
|
|
|
65
71
|
const suffix = markerStyle === "bookend"
|
|
@@ -81,8 +87,10 @@ export function buildPromptBoxLines(
|
|
|
81
87
|
|
|
82
88
|
const borderStyle = isBorderStyle(options.borderStyle) ? options.borderStyle : "double";
|
|
83
89
|
const g = BORDER_GLYPHS[borderStyle];
|
|
84
|
-
const borderColor =
|
|
85
|
-
const
|
|
90
|
+
const borderColor = isPromptBorderColor(options.borderColor) ? options.borderColor : DEFAULT_SETTINGS.decorations.borderColor;
|
|
91
|
+
const thinkingLevel = isThinkingLevel(options.thinkingLevel) ? options.thinkingLevel : undefined;
|
|
92
|
+
const border = getThinkingLevelColorizer(theme, borderColor, thinkingLevel);
|
|
93
|
+
const iconColor = (text: string) => theme.fg("text", text);
|
|
86
94
|
const label = (text: string) => theme.fg("customMessageLabel", text);
|
|
87
95
|
const muted = (text: string) => theme.fg("dim", text);
|
|
88
96
|
const bodyText = (text: string) => theme.fg("customMessageText", text);
|
|
@@ -113,7 +121,7 @@ export function buildPromptBoxLines(
|
|
|
113
121
|
const titleLength = icon ? visibleWidth(`${g.h}${g.h} ${icon} `) : 0;
|
|
114
122
|
const timeLength = time ? time.length + 3 : 0; // time + right-side " ═"
|
|
115
123
|
const topFill = Math.max(0, innerWidth - titleLength - timeLength);
|
|
116
|
-
const iconSeg = icon ? `${border(`${g.h}${g.h} `)}${
|
|
124
|
+
const iconSeg = icon ? `${border(`${g.h}${g.h} `)}${iconColor(icon)}${border(" ")}` : "";
|
|
117
125
|
const timeSeg = time ? `${border(" ")}${muted(time)}${border(` ${g.h}`)}` : "";
|
|
118
126
|
const topLine = border(g.tl) + iconSeg + border(g.h.repeat(topFill)) + timeSeg + border(g.tr);
|
|
119
127
|
const lines = [visibleWidth(topLine) > width ? truncateToWidth(topLine, width) : topLine];
|
|
@@ -138,16 +146,19 @@ export function buildPromptBoxLines(
|
|
|
138
146
|
export const promptBoxRenderer: MessageRenderer<PromptBoxDetails> = (message, _options, theme) => {
|
|
139
147
|
const content = typeof message.content === "string" ? message.content.replace(/(?![\n\t])[\p{Cc}\p{Cf}]/gu, "") : "";
|
|
140
148
|
const details = (message.details ?? {}) as PromptBoxDetails;
|
|
141
|
-
|
|
149
|
+
let cachedWidth: number | undefined;
|
|
150
|
+
let cachedLines: string[] | undefined;
|
|
142
151
|
return {
|
|
143
152
|
render(width: number): string[] {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
linesByWidth.set(width, lines);
|
|
153
|
+
if (cachedWidth !== width) {
|
|
154
|
+
cachedWidth = width;
|
|
155
|
+
cachedLines = buildPromptBoxLines(content, details.submittedAt, width, theme, details);
|
|
148
156
|
}
|
|
149
|
-
return
|
|
157
|
+
return cachedLines!;
|
|
158
|
+
},
|
|
159
|
+
invalidate() {
|
|
160
|
+
cachedWidth = undefined;
|
|
161
|
+
cachedLines = undefined;
|
|
150
162
|
},
|
|
151
|
-
invalidate() { linesByWidth.clear(); },
|
|
152
163
|
};
|
|
153
164
|
};
|
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.
|
|
@@ -321,6 +333,14 @@ export function loadSettings(): DecoratorSettings {
|
|
|
321
333
|
wordPacks.simcity = true;
|
|
322
334
|
}
|
|
323
335
|
const settings = { decorations: mergeGroup(DEFAULT_SETTINGS.decorations, parsed.decorations), features: mergeGroup(DEFAULT_SETTINGS.features, parsed.features), loaderOrder: parseLoaderOrder(parsed.loaderOrder), wordPacks };
|
|
336
|
+
const schemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 1;
|
|
337
|
+
if (schemaVersion < THINKING_LEVEL_COLOR_MIGRATION_VERSION) {
|
|
338
|
+
settings.decorations.spinnerColor = "thinking-level";
|
|
339
|
+
if (settings.decorations.borderColor === "borderAccent") settings.decorations.borderColor = "thinking-level";
|
|
340
|
+
}
|
|
341
|
+
if (schemaVersion < DONE_MARKER_BORDER_COLOR_MIGRATION_VERSION) {
|
|
342
|
+
settings.decorations.doneMarkerBorderColor = "thinking-level";
|
|
343
|
+
}
|
|
324
344
|
for (const entry of MENU_ENTRIES) {
|
|
325
345
|
if (entry.group === "decorations" && entry.cycleEnabledBy && entry.cycleDisabledValue !== undefined && !settings.decorations[entry.cycleEnabledBy]) {
|
|
326
346
|
setDecorationCycleValue(settings.decorations, entry.key, entry.cycleDisabledValue);
|
|
@@ -343,5 +363,5 @@ export function atomicWriteFile(path: string, contents: string): void {
|
|
|
343
363
|
}
|
|
344
364
|
|
|
345
365
|
export function saveSettings(settings: DecoratorSettings): void {
|
|
346
|
-
atomicWriteFile(settingsPath(), `${JSON.stringify(settings, null, 2)}\n`);
|
|
366
|
+
atomicWriteFile(settingsPath(), `${JSON.stringify({ schemaVersion: SETTINGS_SCHEMA_VERSION, ...settings }, null, 2)}\n`);
|
|
347
367
|
}
|
package/src/word-packs.ts
CHANGED
|
@@ -112,7 +112,7 @@ export function selectWorkingTextSelection(enabled: Record<string, boolean>, pac
|
|
|
112
112
|
const total = WORDS.length + enabledWords.length;
|
|
113
113
|
const idx = Math.min(total - 1, Math.floor(fraction * total));
|
|
114
114
|
const entry = idx < WORDS.length ? WORDS[idx] : enabledWords[idx - WORDS.length];
|
|
115
|
-
return { text:
|
|
115
|
+
return { text: entry.present_tense, pastTense: entry.past_tense };
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
export function pickWorkingTextSelection(enabled: Record<string, boolean>, packs: readonly WordPack[]): WorkingTextSelection {
|
package/src/words.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Activity word list for the randomized "Working
|
|
2
|
+
* Activity word list for the randomized "Working" indicator.
|
|
3
3
|
*
|
|
4
4
|
* A random word is picked on prompt and on tool execution start
|
|
5
|
-
* (see
|
|
5
|
+
* (see SessionManager in src/session.ts).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
export interface WordEntry {
|
|
@@ -10,14 +10,6 @@ export interface WordEntry {
|
|
|
10
10
|
past_tense: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export function pickRawWord(): WordEntry {
|
|
14
|
-
return WORDS[Math.floor(Math.random() * WORDS.length)]!;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function pickRandomWord(): string {
|
|
18
|
-
return `${pickRawWord().present_tense}…`;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
13
|
export const WORDS: WordEntry[] = [
|
|
22
14
|
{ present_tense: "Accomplishing", past_tense: "Accomplished" },
|
|
23
15
|
{ present_tense: "Actioning", past_tense: "Actioned" },
|