@underactive/pi-topping 0.6.3 → 0.6.5
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 +84 -78
- package/package.json +1 -1
- package/src/format.ts +13 -7
- package/src/preview.ts +9 -4
- package/src/session.ts +136 -28
- package/src/settings.ts +11 -2
- package/src/util.ts +9 -0
- package/src/word-packs.ts +4 -3
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ We garnish our pies. It seemed rude not to extend Pi the same courtesy. This is
|
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
**“Working” Loader** — animated spinner (with color choice), randomized activity word, optional word packs, shimmer
|
|
9
|
+
**“Working” Loader** — animated spinner (with color choice), randomized activity word, optional word packs, shimmer, token activity monitor, elapsed timer, output token display, live output-token rate, and the response model when it meaningfully differs from the selected model — all arrangeable left to right. During a blocking extension prompt, the loader switches to a stable dim waiting line with a pulsing indicator, then resumes afterward.
|
|
10
10
|
|
|
11
11
|

|
|
12
12
|
|
|
@@ -14,79 +14,91 @@ We garnish our pies. It seemed rude not to extend Pi the same courtesy. This is
|
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pi install npm:@underactive/pi-topping
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Restart Pi (or run `/reload`) to pick it up.
|
|
24
|
+
|
|
17
25
|
## Settings
|
|
18
26
|
|
|
19
27
|
Run `/topping-settings` (TUI only) to customize your toppings. Settings persist to
|
|
20
28
|
`~/.pi/agent/pi-topping/settings.json`; hand edits are tolerated on load.
|
|
21
29
|
|
|
22
30
|
```
|
|
23
|
-
╔═[ Pi Topping: Settings ]
|
|
24
|
-
╟─ Preview
|
|
25
|
-
║
|
|
26
|
-
║ ⠋ Crafting… ⣠⣤⣶⣶⣤⣠⣀⢀ 28
|
|
27
|
-
║
|
|
28
|
-
╟─ User Prompt
|
|
29
|
-
║ [■] High-vis prompt ON
|
|
30
|
-
║ [■] Border style ‹ double ›
|
|
31
|
-
║ [■] Border color ‹ borderAccent ›
|
|
32
|
-
║ [■] Pi icon ON
|
|
33
|
-
║ [■] Timestamp ON
|
|
34
|
-
║ [■] Provider ON
|
|
35
|
-
║ [■] Model ON
|
|
36
|
-
║
|
|
37
|
-
╟─ “Working” Loader
|
|
38
|
-
║ [■] Animated spinner ON
|
|
39
|
-
║ ❯ [■] Animated spinner color ‹ accent ›
|
|
40
|
-
║ [■] Randomize “Working” text ON
|
|
41
|
-
║ [■] Text shimmer ON
|
|
42
|
-
║ [■] Invert shimmer OFF
|
|
43
|
-
║ [■] Text shimmer direction ‹ Left to Right ›
|
|
44
|
-
║ [■] Text shimmer speed ‹ Normal ›
|
|
45
|
-
║ [■] Token activity monitor ON
|
|
46
|
-
║ [■] Token activity monitor color ‹ accent ›
|
|
47
|
-
║ [■] Token activity monitor direction ‹ Right to Left ›
|
|
48
|
-
║ [■] Token activity monitor dimmed OFF
|
|
49
|
-
║ [■] Elapsed time since prompt ON
|
|
50
|
-
║ [■] Show output tokens ON
|
|
51
|
-
║ [■] Token rate ON
|
|
52
|
-
║ [■] Token rate color ‹ warning ›
|
|
53
|
-
║ [■] Token rate dimmed OFF
|
|
54
|
-
║
|
|
55
|
-
|
|
56
|
-
║ [
|
|
57
|
-
║
|
|
58
|
-
|
|
59
|
-
║ [ ]
|
|
60
|
-
║ [ ]
|
|
61
|
-
║ [ ]
|
|
62
|
-
║
|
|
63
|
-
|
|
64
|
-
║ [
|
|
65
|
-
║ [
|
|
66
|
-
║
|
|
67
|
-
|
|
68
|
-
║ [■]
|
|
69
|
-
║ [■]
|
|
70
|
-
║ [■]
|
|
71
|
-
║ [■]
|
|
72
|
-
║
|
|
73
|
-
|
|
74
|
-
║ [
|
|
75
|
-
║ [
|
|
76
|
-
║
|
|
77
|
-
|
|
78
|
-
║ [ ]
|
|
79
|
-
║ [ ]
|
|
80
|
-
║ [ ]
|
|
81
|
-
║ [ ]
|
|
82
|
-
║ [ ]
|
|
83
|
-
║
|
|
84
|
-
|
|
85
|
-
║ [
|
|
86
|
-
|
|
87
|
-
║
|
|
88
|
-
|
|
89
|
-
|
|
31
|
+
╔═[ Pi Topping: Settings ]══════════════════════════════════════╗
|
|
32
|
+
╟─ Preview ─────────────────────────────────────────────────────╢
|
|
33
|
+
║ ║
|
|
34
|
+
║ ⠋ Crafting… ⣠⣤⣶⣶⣤⣠⣀⢀ 28 tps · 3s · ↓ 84 tokens · test-model ║
|
|
35
|
+
║ ║
|
|
36
|
+
╟─ User Prompt ─────────────────────────────────────────────────╢
|
|
37
|
+
║ [■] High-vis prompt ON ║
|
|
38
|
+
║ [■] Border style ‹ double › ║
|
|
39
|
+
║ [■] Border color ‹ borderAccent › ║
|
|
40
|
+
║ [■] Pi icon ON ║
|
|
41
|
+
║ [■] Timestamp ON ║
|
|
42
|
+
║ [■] Provider ON ║
|
|
43
|
+
║ [■] Model ON ║
|
|
44
|
+
║ ║
|
|
45
|
+
╟─ “Working” Loader ────────────────────────────────────────────╢
|
|
46
|
+
║ [■] Animated spinner ON ║
|
|
47
|
+
║ ❯ [■] Animated spinner color ‹ accent › ║
|
|
48
|
+
║ [■] Randomize “Working” text ON ║
|
|
49
|
+
║ [■] Text shimmer ON ║
|
|
50
|
+
║ [■] Invert shimmer OFF ║
|
|
51
|
+
║ [■] Text shimmer direction ‹ Left to Right › ║
|
|
52
|
+
║ [■] Text shimmer speed ‹ Normal › ║
|
|
53
|
+
║ [■] Token activity monitor ON ║
|
|
54
|
+
║ [■] Token activity monitor color ‹ accent › ║
|
|
55
|
+
║ [■] Token activity monitor direction ‹ Right to Left › ║
|
|
56
|
+
║ [■] Token activity monitor dimmed OFF ║
|
|
57
|
+
║ [■] Elapsed time since prompt ON ║
|
|
58
|
+
║ [■] Show output tokens ON ║
|
|
59
|
+
║ [■] Token rate ON ║
|
|
60
|
+
║ [■] Token rate color ‹ warning › ║
|
|
61
|
+
║ [■] Token rate dimmed OFF ║
|
|
62
|
+
║ [■] Response model ON ║
|
|
63
|
+
║ [■] Response model color ‹ accent › ║
|
|
64
|
+
║ [■] Response model dimmed OFF ║
|
|
65
|
+
║ ║
|
|
66
|
+
╟─ Elements Order ──────────────────────────────────────────────╢
|
|
67
|
+
║ [ ] Animated spinner ║
|
|
68
|
+
║ [ ] “Working” text ║
|
|
69
|
+
║ [ ] Token activity monitor ║
|
|
70
|
+
║ [ ] Token rate ║
|
|
71
|
+
║ [ ] Elapsed time ║
|
|
72
|
+
║ [ ] Output tokens ║
|
|
73
|
+
║ [ ] Response model ║
|
|
74
|
+
║ ║
|
|
75
|
+
╟─ Completion Marker ───────────────────────────────────────────╢
|
|
76
|
+
║ [■] Show completion marker ON ║
|
|
77
|
+
║ [■] Marker style ‹ elite › ║
|
|
78
|
+
║ [■] Border style ‹ none › ║
|
|
79
|
+
║ [■] Border color ‹ borderAccent › ║
|
|
80
|
+
║ [■] Pi icon ON ║
|
|
81
|
+
║ [■] Randomize “Worked” text ON ║
|
|
82
|
+
║ [■] Tokens spent ON ║
|
|
83
|
+
║ [■] Mid-turn inputs ON ║
|
|
84
|
+
║ ║
|
|
85
|
+
╟─ Word Packs ──────────────────────────────────────────────────╢
|
|
86
|
+
║ [ ] Doctor Who OFF ║
|
|
87
|
+
║ [ ] Firefly OFF ║
|
|
88
|
+
║ [ ] Hitchhiker's Guide OFF ║
|
|
89
|
+
║ [ ] The Lord of the Rings OFF ║
|
|
90
|
+
║ [ ] The Matrix OFF ║
|
|
91
|
+
║ [ ] Portal OFF ║
|
|
92
|
+
║ [ ] SimCity OFF ║
|
|
93
|
+
║ [ ] Star Trek OFF ║
|
|
94
|
+
║ [ ] Star Wars OFF ║
|
|
95
|
+
║ ║
|
|
96
|
+
╟─ Options ─────────────────────────────────────────────────────╢
|
|
97
|
+
║ [■] Use NerdFont icons ON ║
|
|
98
|
+
╟───────────────────────────────────────────────────────────────╢
|
|
99
|
+
║ ↑↓ move PgUp/PgDn page ←→ select ║
|
|
100
|
+
║ ␣ toggle ⏎ apply esc cancel ║
|
|
101
|
+
╚═══════════════════════════════════════════════════════[ 9/50 ]╝
|
|
90
102
|
```
|
|
91
103
|
|
|
92
104
|
### Word packs
|
|
@@ -108,24 +120,18 @@ Bundled packs live in [`wordpacks/`](wordpacks/). Copy `wordpacks/doctor-who.jso
|
|
|
108
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.
|
|
109
121
|
|
|
110
122
|
Under **Elements Order**, press `␣` to grab a row, then `↑`/`↓` to slide that element
|
|
111
|
-
left or right within the loader. Elapsed time, output tokens,
|
|
123
|
+
left or right within the loader. Elapsed time, output tokens, token rate, and response model are joined with
|
|
112
124
|
`·` separators whenever adjacent. When reordering separates them, each adjacent run is rendered
|
|
113
125
|
without a separator before or after it.
|
|
114
126
|
|
|
115
|
-
The `N
|
|
127
|
+
The `N tps` token rate uses the selected theme color and
|
|
116
128
|
remains reorderable. Active rates below 1,000 are padded to three characters so updates do not shift the other
|
|
117
|
-
segments; larger rates may shift later segments. When inactive, it remains as the dim `---
|
|
129
|
+
segments; larger rates may shift later segments. When inactive, it remains as the dim `--- tps` placeholder. After its last update it holds
|
|
118
130
|
full brightness for 1.5 seconds, then fades through five theme-aware shades to the dim text color over
|
|
119
131
|
the next 0.25 seconds before returning to the placeholder; a new count restores full brightness and
|
|
120
132
|
restarts the cycle.
|
|
121
133
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
pi install npm:@underactive/pi-topping
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
Restart Pi (or run `/reload`) to pick it up.
|
|
134
|
+
The response model is captured from assistant responses, displayed as its sanitized value with no label only when it meaningfully differs from the selected model, and defaults to the final loader detail after output tokens. Equivalent local paths and decorated filenames are suppressed. It can be hidden, recolored, permanently dimmed, or reordered. After the agent settles, its final value remains visible for 3 seconds, fades through five theme-aware shades over 0.5 seconds, then clears; new work cancels the pending fade.
|
|
129
135
|
|
|
130
136
|
## Requirements
|
|
131
137
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@underactive/pi-topping",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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/format.ts
CHANGED
|
@@ -19,13 +19,13 @@ export const SHIMMER_INTERVAL_MS = 50;
|
|
|
19
19
|
export const ELAPSED_INTERVAL_MS = 1_000;
|
|
20
20
|
|
|
21
21
|
/** A user-orderable piece of the working indicator. */
|
|
22
|
-
export type LoaderElement = "spinner" | "text" | "meter" | "elapsed" | "tokens" | "tokenRate";
|
|
22
|
+
export type LoaderElement = "spinner" | "text" | "meter" | "elapsed" | "tokens" | "tokenRate" | "responseModel";
|
|
23
23
|
|
|
24
24
|
/** Default first-use working-indicator element order. */
|
|
25
|
-
export const DEFAULT_LOADER_ORDER: readonly LoaderElement[] = ["spinner", "text", "meter", "tokenRate", "elapsed", "tokens"];
|
|
25
|
+
export const DEFAULT_LOADER_ORDER: readonly LoaderElement[] = ["spinner", "text", "meter", "tokenRate", "elapsed", "tokens", "responseModel"];
|
|
26
26
|
|
|
27
27
|
/** Elements share a detail group; separators and non-rate details are dimmed. */
|
|
28
|
-
const DETAIL_ELEMENTS: ReadonlySet<LoaderElement> = new Set(["elapsed", "tokens", "tokenRate"]);
|
|
28
|
+
const DETAIL_ELEMENTS: ReadonlySet<LoaderElement> = new Set(["elapsed", "tokens", "tokenRate", "responseModel"]);
|
|
29
29
|
|
|
30
30
|
const TOKEN_UNITS = [
|
|
31
31
|
{ threshold: 10_000, divisor: 1_000, decimals: 1, suffix: "k" },
|
|
@@ -52,12 +52,12 @@ export function formatTokens(count: number): string {
|
|
|
52
52
|
const TOKEN_RATE_DIGITS = 3;
|
|
53
53
|
|
|
54
54
|
/** Placeholder shown when the working indicator has no active token-rate sample. */
|
|
55
|
-
export const TOKEN_RATE_PLACEHOLDER = `${"-".repeat(TOKEN_RATE_DIGITS)}
|
|
55
|
+
export const TOKEN_RATE_PLACEHOLDER = `${"-".repeat(TOKEN_RATE_DIGITS)} tps`;
|
|
56
56
|
|
|
57
57
|
/** Format an output-token throughput estimate for the working indicator. */
|
|
58
58
|
export function formatTokenRate(rate: number): string {
|
|
59
59
|
const rounded = Math.round(rate);
|
|
60
|
-
return rounded === 0 ? "" : `${rounded.toString().padStart(TOKEN_RATE_DIGITS)}
|
|
60
|
+
return rounded === 0 ? "" : `${rounded.toString().padStart(TOKEN_RATE_DIGITS)} tps`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
@@ -74,6 +74,12 @@ export function dimAttribute(text: string): string {
|
|
|
74
74
|
/** Number of discrete, eased warning-to-dim shades used for the token-rate fade. */
|
|
75
75
|
export const TOKEN_RATE_FADE_SHADE_COUNT = 5;
|
|
76
76
|
|
|
77
|
+
/** Milliseconds a settled response model remains fully visible before fading. */
|
|
78
|
+
export const RESPONSE_MODEL_HOLD_MS = 3_000;
|
|
79
|
+
|
|
80
|
+
/** Milliseconds over which a settled response model fades through the theme shades. */
|
|
81
|
+
export const RESPONSE_MODEL_FADE_MS = 500;
|
|
82
|
+
|
|
77
83
|
/** Render one token-rate fade shade by blending the selected color toward the active theme's dim color. */
|
|
78
84
|
export function fadeThemeColorString(
|
|
79
85
|
text: string,
|
|
@@ -144,8 +150,8 @@ export function buildWorkingMessage(
|
|
|
144
150
|
const value = parts[element];
|
|
145
151
|
if (!value) continue;
|
|
146
152
|
if (DETAIL_ELEMENTS.has(element)) {
|
|
147
|
-
// tokenRate
|
|
148
|
-
details.push(element === "tokenRate" ? value : theme.fg("dim", value));
|
|
153
|
+
// tokenRate and responseModel arrive pre-colored; dimming them here would replace their configured colors.
|
|
154
|
+
details.push(element === "tokenRate" || element === "responseModel" ? value : theme.fg("dim", value));
|
|
149
155
|
continue;
|
|
150
156
|
}
|
|
151
157
|
flushDetails();
|
package/src/preview.ts
CHANGED
|
@@ -6,8 +6,8 @@ import { buildCompletionMarkerContent, buildCompletionMarkerLine, buildPromptBox
|
|
|
6
6
|
import { DEFAULT_SETTINGS, fromCycleDirection, fromCycleSpeed, isBorderStyle, isDoneMarkerBorderStyle, isDoneMarkerStyle, isSettingColor, LOADER_ORDER_ID, MENU_ENTRIES, parseLoaderOrder } from "./settings.ts";
|
|
7
7
|
import { isWordPackEnabled, selectWorkingTextSelection, wordPacksPath, type WordPack } from "./word-packs.ts";
|
|
8
8
|
import { pickRandomWord } from "./words.ts";
|
|
9
|
-
// Simulated load for the menu preview: a 2.4s cosine wave peaking at 46
|
|
10
|
-
// flat 28
|
|
9
|
+
// Simulated load for the menu preview: a 2.4s cosine wave peaking at 46 tps for the meter,
|
|
10
|
+
// flat 28 tps for the token readouts.
|
|
11
11
|
const METER_PERIOD_MS = 2400, METER_PEAK_RATE = 46, TOKEN_RATE_PER_SEC = 28;
|
|
12
12
|
const PROMPT_IDS = new Set(MENU_ENTRIES.filter(entry => entry.section === "User Prompt").map(entry => entry.id));
|
|
13
13
|
const MARKER_IDS = new Set(MENU_ENTRIES.filter(entry => entry.section === "Completion Marker").map(entry => entry.id));
|
|
@@ -51,7 +51,7 @@ export class PreviewRenderer {
|
|
|
51
51
|
this.#meter.push(rateToLevel(meterRate(elapsedMs)));
|
|
52
52
|
this.#lastMeterUpdate = elapsedMs;
|
|
53
53
|
}
|
|
54
|
-
const features = { substituteDefaultMessage: values.substituteDefaultMessage !== false, elapsedTime: values.elapsedTime !== false, outputTokens: values.outputTokens !== false, tokenRate: values.showTokenRate !== false };
|
|
54
|
+
const features = { substituteDefaultMessage: values.substituteDefaultMessage !== false, elapsedTime: values.elapsedTime !== false, outputTokens: values.outputTokens !== false, tokenRate: values.showTokenRate !== false, responseModel: values.showResponseModel !== false };
|
|
55
55
|
const decorations = { shimmer: values.shimmer !== false, shimmerInverted: values.shimmerInverted === true, tokenActivityMonitor: values.tokenActivityMonitor !== false };
|
|
56
56
|
let spinnerColor = DEFAULT_SETTINGS.decorations.spinnerColor;
|
|
57
57
|
if (values.spinnerColorEnabled !== false && isSettingColor(values.spinnerColor)) {
|
|
@@ -62,8 +62,12 @@ export class PreviewRenderer {
|
|
|
62
62
|
spinner = this.#ctx.ui.theme.fg(spinnerColor, SPINNER_FRAMES[Math.floor(elapsedMs / SPINNER_FRAME_MS) % SPINNER_FRAMES.length]!);
|
|
63
63
|
}
|
|
64
64
|
const order = parseLoaderOrder(values[LOADER_ORDER_ID]);
|
|
65
|
+
let responseModelColor = DEFAULT_SETTINGS.decorations.responseModelColor;
|
|
66
|
+
if (isSettingColor(values.responseModelColor)) responseModelColor = values.responseModelColor;
|
|
67
|
+
const responseModelColored = features.responseModel ? this.#ctx.ui.theme.fg(responseModelColor, "test-model") : "";
|
|
68
|
+
const responseModel = responseModelColored && values.responseModelDimmed === true ? dimAttribute(responseModelColored) : responseModelColored;
|
|
65
69
|
if (isFullyDefaultAppearance(features, decorations)) {
|
|
66
|
-
return buildWorkingMessage(this.#ctx.ui.theme, { spinner, text: this.#ctx.ui.theme.fg("dim", DEFAULT_WORKING_WORD) }, order);
|
|
70
|
+
return buildWorkingMessage(this.#ctx.ui.theme, { spinner, text: this.#ctx.ui.theme.fg("dim", DEFAULT_WORKING_WORD), responseModel }, order);
|
|
67
71
|
}
|
|
68
72
|
let word = DEFAULT_WORKING_WORD;
|
|
69
73
|
if (features.substituteDefaultMessage) {
|
|
@@ -121,6 +125,7 @@ export class PreviewRenderer {
|
|
|
121
125
|
elapsed,
|
|
122
126
|
tokens,
|
|
123
127
|
tokenRate,
|
|
128
|
+
responseModel,
|
|
124
129
|
}, order);
|
|
125
130
|
}
|
|
126
131
|
private packValues(values: Record<string, boolean | string>): Record<string, boolean> {
|
package/src/session.ts
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
formatTokens,
|
|
26
26
|
isFullyDefaultAppearance,
|
|
27
27
|
METER_INTERVAL_MS,
|
|
28
|
+
RESPONSE_MODEL_FADE_MS,
|
|
29
|
+
RESPONSE_MODEL_HOLD_MS,
|
|
28
30
|
SHIMMER_INTERVAL_MS,
|
|
29
31
|
shimmerString,
|
|
30
32
|
SPINNER_FRAME_MS,
|
|
@@ -40,7 +42,7 @@ import { applyMenuResult, buildMenuSections, loadSettings, saveSettings, type Se
|
|
|
40
42
|
import { notifyMissingToppingsOnce } from "./toppings.ts";
|
|
41
43
|
import { PreviewRenderer } from "./preview.ts";
|
|
42
44
|
import { buildCompletionMarkerContent, buildCompletionMarkerLine, PROMPT_BOX_TYPE, promptBoxRenderer, type PromptBoxDetails } from "./prompt-decorator.ts";
|
|
43
|
-
import { stripControlChars } from "./util.ts";
|
|
45
|
+
import { modelsResemble, stripControlChars } from "./util.ts";
|
|
44
46
|
import { loadBundledWordPacks, loadUserWordPacks, pickWorkingTextSelection, type WorkingTextSelection, type WordPack } from "./word-packs.ts";
|
|
45
47
|
|
|
46
48
|
type MessageStartEvent = Extract<ExtensionEvent, { type: "message_start" }>;
|
|
@@ -52,6 +54,7 @@ type UIPromptEndEvent = Extract<ExtensionEvent, { type: "ui_prompt_end" }>;
|
|
|
52
54
|
type UIPromptKind = UIPromptStartEvent["kind"];
|
|
53
55
|
|
|
54
56
|
const DONE_ENTRY_TYPE = "pi-topping-done";
|
|
57
|
+
const RESPONSE_MODEL_STATUS_KEY = "pi-topping-response-model";
|
|
55
58
|
const WAITING_LABELS: Record<UIPromptKind, string> = {
|
|
56
59
|
select: "Waiting for selection",
|
|
57
60
|
confirm: "Waiting for confirmation",
|
|
@@ -82,6 +85,12 @@ interface SessionState {
|
|
|
82
85
|
rateTracker: TokRateTracker;
|
|
83
86
|
lastTokenRateSampledAt: number;
|
|
84
87
|
lastTokenRateTotal: number;
|
|
88
|
+
responseModel: string;
|
|
89
|
+
/** Last raw responseModel value seen, or the NOT_SENT sentinel before the first call. */
|
|
90
|
+
lastResponseModelRaw: unknown;
|
|
91
|
+
responseModelHoldTimer: ReturnType<typeof setTimeout> | null;
|
|
92
|
+
responseModelFadeTimer: ReturnType<typeof setInterval> | null;
|
|
93
|
+
responseModelFadeGeneration: number;
|
|
85
94
|
tokenRateText: string;
|
|
86
95
|
tokenRateFadeStartsAt: number;
|
|
87
96
|
timer: ReturnType<typeof setInterval> | null;
|
|
@@ -115,6 +124,11 @@ function makeFreshState(): SessionState {
|
|
|
115
124
|
rateTracker: new TokRateTracker(),
|
|
116
125
|
lastTokenRateSampledAt: 0,
|
|
117
126
|
lastTokenRateTotal: 0,
|
|
127
|
+
responseModel: "",
|
|
128
|
+
lastResponseModelRaw: NOT_SENT,
|
|
129
|
+
responseModelHoldTimer: null,
|
|
130
|
+
responseModelFadeTimer: null,
|
|
131
|
+
responseModelFadeGeneration: 0,
|
|
118
132
|
tokenRateText: "",
|
|
119
133
|
tokenRateFadeStartsAt: 0,
|
|
120
134
|
timer: null,
|
|
@@ -137,6 +151,7 @@ export class SessionManager {
|
|
|
137
151
|
#settings = loadSettings();
|
|
138
152
|
#userPacks: WordPack[] = [];
|
|
139
153
|
#bundledPacks = loadBundledWordPacks();
|
|
154
|
+
#allPacks: WordPack[] = [...this.#bundledPacks];
|
|
140
155
|
#currentCtx: ExtensionContext | null = null;
|
|
141
156
|
readonly #pi: ExtensionAPI;
|
|
142
157
|
|
|
@@ -145,13 +160,15 @@ export class SessionManager {
|
|
|
145
160
|
}
|
|
146
161
|
|
|
147
162
|
#onSessionStart = async (_e: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
163
|
+
this.#currentCtx = ctx;
|
|
148
164
|
this.stopTimer();
|
|
165
|
+
this.cancelResponseModelFade(ctx);
|
|
149
166
|
this.#counter.reset();
|
|
150
167
|
this.#state = makeFreshState();
|
|
151
168
|
this.#settings = loadSettings();
|
|
152
169
|
this.#userPacks = loadUserWordPacks();
|
|
170
|
+
this.#allPacks = [...this.#bundledPacks, ...this.#userPacks];
|
|
153
171
|
this.#state.activityMeter.setDirection(this.#settings.decorations.meterDirection);
|
|
154
|
-
this.#currentCtx = ctx;
|
|
155
172
|
if (this.usable(ctx)) {
|
|
156
173
|
this.applyIndicator(ctx);
|
|
157
174
|
if (isSiblingSetupEnabled()) notifyMissingToppingsOnce(this.#pi, ctx.ui);
|
|
@@ -165,32 +182,38 @@ export class SessionManager {
|
|
|
165
182
|
if (!event.streamingBehavior && !this.#state.busy) this.resetTurn(Date.now());
|
|
166
183
|
else if (event.streamingBehavior && event.source !== "extension") this.#state.midTurnInputs++;
|
|
167
184
|
if (this.#settings.decorations.decorateUserPrompt && this.shouldDecorate(event)) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
try {
|
|
186
|
+
await this.#pi.sendMessage<PromptBoxDetails>(
|
|
187
|
+
{
|
|
188
|
+
customType: PROMPT_BOX_TYPE,
|
|
189
|
+
content: event.text,
|
|
190
|
+
display: true,
|
|
191
|
+
details: {
|
|
192
|
+
submittedAt: Date.now(),
|
|
193
|
+
showIcon: this.#settings.decorations.promptIcon,
|
|
194
|
+
showTimestamp: this.#settings.decorations.promptTimestamp,
|
|
195
|
+
showProvider: this.#settings.decorations.promptProvider,
|
|
196
|
+
showModel: this.#settings.decorations.promptModel,
|
|
197
|
+
icon: this.#settings.decorations.useNerdFont ? "" : "π",
|
|
198
|
+
provider: ctx.model?.provider,
|
|
199
|
+
model: ctx.model?.id,
|
|
200
|
+
borderColor: this.#settings.decorations.borderColor,
|
|
201
|
+
borderStyle: this.#settings.decorations.borderStyle,
|
|
202
|
+
},
|
|
184
203
|
},
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
204
|
+
{ triggerTurn: true },
|
|
205
|
+
);
|
|
206
|
+
return { action: "handled" };
|
|
207
|
+
} catch {
|
|
208
|
+
ctx.ui.notify("Failed to submit prompt", "error");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
189
211
|
}
|
|
190
212
|
};
|
|
191
213
|
|
|
192
214
|
#onAgentStart = async (_e: AgentStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
193
215
|
this.#currentCtx = ctx;
|
|
216
|
+
this.cancelResponseModelFade(ctx);
|
|
194
217
|
if (!this.usable(ctx)) return;
|
|
195
218
|
if (!this.#state.startTime) this.resetTurn(Date.now());
|
|
196
219
|
this.#state.busy = true;
|
|
@@ -203,6 +226,7 @@ export class SessionManager {
|
|
|
203
226
|
if (this.usable(ctx) && event.message.role === "assistant") {
|
|
204
227
|
this.#state.liveTokens = 0;
|
|
205
228
|
this.#counter.reset();
|
|
229
|
+
this.updateResponseModel((event.message as { responseModel?: unknown }).responseModel);
|
|
206
230
|
}
|
|
207
231
|
};
|
|
208
232
|
|
|
@@ -216,13 +240,20 @@ export class SessionManager {
|
|
|
216
240
|
) {
|
|
217
241
|
this.#state.liveTokens += this.#counter.count(assistantEvent.delta, assistantEvent.type);
|
|
218
242
|
}
|
|
243
|
+
if (this.usable(ctx)) {
|
|
244
|
+
const rawResponseModel = (assistantEvent as { partial?: { responseModel?: unknown } } | undefined)?.partial?.responseModel
|
|
245
|
+
?? (event.message as { responseModel?: unknown } | undefined)?.responseModel;
|
|
246
|
+
this.updateResponseModel(rawResponseModel);
|
|
247
|
+
}
|
|
219
248
|
};
|
|
220
249
|
|
|
221
250
|
#onMessageEnd = async (event: MessageEndEvent, ctx: ExtensionContext): Promise<void> => {
|
|
222
251
|
this.#currentCtx = ctx;
|
|
223
252
|
if (!this.usable(ctx) || event.message.role !== "assistant") return;
|
|
224
253
|
|
|
225
|
-
const
|
|
254
|
+
const rawTokens = event.message.usage?.output;
|
|
255
|
+
const exactTokens =
|
|
256
|
+
typeof rawTokens === "number" && Number.isFinite(rawTokens) && rawTokens >= 0 ? rawTokens : undefined;
|
|
226
257
|
this.#state.confirmTokens += exactTokens ?? this.#state.liveTokens;
|
|
227
258
|
if (exactTokens !== undefined) {
|
|
228
259
|
// Only reset the sampling state here, not tokenRateText/tokenRateFadeStartsAt:
|
|
@@ -233,6 +264,7 @@ export class SessionManager {
|
|
|
233
264
|
}
|
|
234
265
|
this.#state.liveTokens = 0;
|
|
235
266
|
this.#counter.reset();
|
|
267
|
+
this.updateResponseModel((event.message as { responseModel?: unknown }).responseModel);
|
|
236
268
|
};
|
|
237
269
|
|
|
238
270
|
#onToolExecutionStart = async (_e: ToolExecutionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
@@ -267,13 +299,17 @@ export class SessionManager {
|
|
|
267
299
|
|
|
268
300
|
const hadPrompt = !!this.#state.startTime;
|
|
269
301
|
const elapsedMs = hadPrompt ? Date.now() - this.#state.startTime : 0;
|
|
302
|
+
const responseModel = this.#settings.features.responseModel ? this.#state.responseModel : "";
|
|
303
|
+
const responseModelColor = this.#settings.decorations.responseModelColor;
|
|
304
|
+
const responseModelDimmed = this.#settings.decorations.responseModelDimmed;
|
|
270
305
|
this.#state.busy = false;
|
|
271
306
|
this.#state.waiting = null;
|
|
272
307
|
this.#state.startTime = 0;
|
|
273
308
|
this.#counter.reset();
|
|
274
309
|
this.stopTimer();
|
|
275
|
-
ctx.ui.setWorkingMessage();
|
|
276
310
|
this.applyIndicator(ctx);
|
|
311
|
+
ctx.ui.setWorkingMessage();
|
|
312
|
+
if (responseModel) this.startResponseModelFade(ctx, responseModel, responseModelColor, responseModelDimmed);
|
|
277
313
|
this.#state.activityMeter.reset();
|
|
278
314
|
resetTokenRateState(this.#state);
|
|
279
315
|
this.#state.lastMessage = NOT_SENT;
|
|
@@ -288,8 +324,9 @@ export class SessionManager {
|
|
|
288
324
|
this.#currentCtx = null;
|
|
289
325
|
};
|
|
290
326
|
|
|
291
|
-
#onSessionShutdown = async (_e: SessionShutdownEvent,
|
|
327
|
+
#onSessionShutdown = async (_e: SessionShutdownEvent, ctx: ExtensionContext): Promise<void> => {
|
|
292
328
|
this.#state.waiting = null;
|
|
329
|
+
this.cancelResponseModelFade(ctx);
|
|
293
330
|
this.stopTimer();
|
|
294
331
|
};
|
|
295
332
|
|
|
@@ -413,6 +450,68 @@ export class SessionManager {
|
|
|
413
450
|
}
|
|
414
451
|
}
|
|
415
452
|
|
|
453
|
+
private updateResponseModel(raw: unknown): void {
|
|
454
|
+
if (raw === this.#state.lastResponseModelRaw) return;
|
|
455
|
+
this.#state.lastResponseModelRaw = raw;
|
|
456
|
+
const responseModel = typeof raw === "string" ? stripControlChars(raw).trim() : "";
|
|
457
|
+
const selectedModel = this.#currentCtx?.model?.id;
|
|
458
|
+
if (responseModel && modelsResemble(selectedModel, responseModel)) {
|
|
459
|
+
if (this.#state.responseModel) {
|
|
460
|
+
this.#state.responseModel = "";
|
|
461
|
+
this.tick();
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
if (responseModel && responseModel !== this.#state.responseModel) {
|
|
466
|
+
this.#state.responseModel = responseModel;
|
|
467
|
+
this.tick();
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private cancelResponseModelFade(ctx?: ExtensionContext | null): void {
|
|
472
|
+
const state = this.#state;
|
|
473
|
+
state.responseModelFadeGeneration++;
|
|
474
|
+
if (state.responseModelHoldTimer) {
|
|
475
|
+
clearTimeout(state.responseModelHoldTimer);
|
|
476
|
+
state.responseModelHoldTimer = null;
|
|
477
|
+
}
|
|
478
|
+
if (state.responseModelFadeTimer) {
|
|
479
|
+
clearInterval(state.responseModelFadeTimer);
|
|
480
|
+
state.responseModelFadeTimer = null;
|
|
481
|
+
}
|
|
482
|
+
const activeCtx = ctx ?? this.#currentCtx;
|
|
483
|
+
if (activeCtx && this.usable(activeCtx)) {
|
|
484
|
+
activeCtx.ui.setStatus(RESPONSE_MODEL_STATUS_KEY, undefined);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
private startResponseModelFade(ctx: ExtensionContext, model: string, color: SettingColor, dimmed: boolean): void {
|
|
489
|
+
this.cancelResponseModelFade(ctx);
|
|
490
|
+
const generation = this.#state.responseModelFadeGeneration;
|
|
491
|
+
const render = (shade?: number): void => {
|
|
492
|
+
const colored = shade === undefined
|
|
493
|
+
? ctx.ui.theme.fg(color, model)
|
|
494
|
+
: fadeThemeColorString(model, shade, ctx.ui.theme, color);
|
|
495
|
+
const responseModel = dimmed ? dimAttribute(colored) : colored;
|
|
496
|
+
ctx.ui.setStatus(RESPONSE_MODEL_STATUS_KEY, responseModel);
|
|
497
|
+
};
|
|
498
|
+
render();
|
|
499
|
+
this.#state.responseModelHoldTimer = setTimeout(() => {
|
|
500
|
+
if (this.#state.responseModelFadeGeneration !== generation) return;
|
|
501
|
+
this.#state.responseModelHoldTimer = null;
|
|
502
|
+
let shade = 0;
|
|
503
|
+
render(shade++);
|
|
504
|
+
this.#state.responseModelFadeTimer = setInterval(() => {
|
|
505
|
+
if (this.#state.responseModelFadeGeneration !== generation) return;
|
|
506
|
+
if (shade >= TOKEN_RATE_FADE_SHADE_COUNT) {
|
|
507
|
+
this.cancelResponseModelFade(ctx);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
render(shade++);
|
|
511
|
+
}, RESPONSE_MODEL_FADE_MS / TOKEN_RATE_FADE_SHADE_COUNT);
|
|
512
|
+
}, RESPONSE_MODEL_HOLD_MS);
|
|
513
|
+
}
|
|
514
|
+
|
|
416
515
|
private startTimer(): void {
|
|
417
516
|
if (this.#state.timer) return;
|
|
418
517
|
|
|
@@ -427,16 +526,19 @@ export class SessionManager {
|
|
|
427
526
|
}
|
|
428
527
|
|
|
429
528
|
private pickWorkingWord(): WorkingTextSelection {
|
|
430
|
-
return pickWorkingTextSelection(this.#settings.wordPacks,
|
|
529
|
+
return pickWorkingTextSelection(this.#settings.wordPacks, this.#allPacks);
|
|
431
530
|
}
|
|
432
531
|
|
|
433
532
|
private resetTurn(now: number): void {
|
|
533
|
+
this.cancelResponseModelFade();
|
|
434
534
|
const state = this.#state;
|
|
435
535
|
state.startTime = now;
|
|
436
536
|
state.shimmerOrigin = now;
|
|
437
537
|
state.workingText = this.pickWorkingWord();
|
|
438
538
|
state.confirmTokens = 0;
|
|
439
539
|
state.liveTokens = 0;
|
|
540
|
+
state.responseModel = "";
|
|
541
|
+
state.lastResponseModelRaw = NOT_SENT;
|
|
440
542
|
state.activityMeter.reset();
|
|
441
543
|
resetTokenRateState(state);
|
|
442
544
|
state.midTurnInputs = 0;
|
|
@@ -473,9 +575,13 @@ export class SessionManager {
|
|
|
473
575
|
const spinner = this.spinnerInMessage()
|
|
474
576
|
? ctx.ui.theme.fg(this.spinnerColor(), SPINNER_FRAMES[Math.floor(now / SPINNER_FRAME_MS) % SPINNER_FRAMES.length]!)
|
|
475
577
|
: "";
|
|
578
|
+
const responseModelColored = features.responseModel && state.responseModel
|
|
579
|
+
? ctx.ui.theme.fg(decorations.responseModelColor, state.responseModel)
|
|
580
|
+
: "";
|
|
581
|
+
const responseModel = responseModelColored && decorations.responseModelDimmed ? dimAttribute(responseModelColored) : responseModelColored;
|
|
476
582
|
if (isFullyDefaultAppearance(features, decorations)) {
|
|
477
|
-
const msg = spinner
|
|
478
|
-
? buildWorkingMessage(ctx.ui.theme, { spinner, text: ctx.ui.theme.fg("dim", DEFAULT_WORKING_WORD) }, this.#settings.loaderOrder)
|
|
583
|
+
const msg = spinner || responseModel
|
|
584
|
+
? buildWorkingMessage(ctx.ui.theme, { spinner, text: ctx.ui.theme.fg("dim", DEFAULT_WORKING_WORD), responseModel }, this.#settings.loaderOrder)
|
|
479
585
|
: undefined;
|
|
480
586
|
if (msg !== state.lastMessage) {
|
|
481
587
|
state.lastMessage = msg;
|
|
@@ -532,6 +638,7 @@ export class SessionManager {
|
|
|
532
638
|
elapsed: features.elapsedTime ? formatElapsed(now - state.startTime) : "",
|
|
533
639
|
tokens: features.outputTokens ? `↓ ${formatTokens(total)} tokens` : "",
|
|
534
640
|
tokenRate: tokenRateStyled,
|
|
641
|
+
responseModel,
|
|
535
642
|
},
|
|
536
643
|
this.#settings.loaderOrder,
|
|
537
644
|
);
|
|
@@ -548,6 +655,7 @@ export class SessionManager {
|
|
|
548
655
|
}
|
|
549
656
|
|
|
550
657
|
this.#userPacks = loadUserWordPacks();
|
|
658
|
+
this.#allPacks = [...this.#bundledPacks, ...this.#userPacks];
|
|
551
659
|
const before = this.indicatorFingerprint();
|
|
552
660
|
const preview = new PreviewRenderer(ctx, [...this.#bundledPacks, ...this.#userPacks]);
|
|
553
661
|
const result = await showMenu<Record<string, boolean | string>>(ctx, {
|
package/src/settings.ts
CHANGED
|
@@ -59,6 +59,8 @@ export interface DecoratorSettings {
|
|
|
59
59
|
meterDimmed: boolean;
|
|
60
60
|
tokenRateColor: SettingColor;
|
|
61
61
|
tokenRateDimmed: boolean;
|
|
62
|
+
responseModelColor: SettingColor;
|
|
63
|
+
responseModelDimmed: boolean;
|
|
62
64
|
promptIcon: boolean;
|
|
63
65
|
promptTimestamp: boolean;
|
|
64
66
|
promptProvider: boolean;
|
|
@@ -70,6 +72,7 @@ export interface DecoratorSettings {
|
|
|
70
72
|
elapsedTime: boolean;
|
|
71
73
|
outputTokens: boolean;
|
|
72
74
|
tokenRate: boolean;
|
|
75
|
+
responseModel: boolean;
|
|
73
76
|
doneMarker: boolean;
|
|
74
77
|
doneMarkerIcon: boolean;
|
|
75
78
|
randomizeDoneMarker: boolean;
|
|
@@ -81,8 +84,8 @@ export interface DecoratorSettings {
|
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
export const DEFAULT_SETTINGS: DecoratorSettings = {
|
|
84
|
-
decorations: { animatedSpinner: true, shimmer: true, shimmerInverted: false, shimmerDirection: "ltr", shimmerDirectionEnabled: true, shimmerSpeed: "normal", shimmerSpeedEnabled: true, tokenActivityMonitor: true, meterDirection: "rtl", meterDirectionEnabled: true, decorateUserPrompt: true, borderColor: "borderAccent", borderColorEnabled: true, borderStyle: "double", borderStyleEnabled: true, doneMarkerBorderStyle: "none", doneMarkerBorderColor: "borderAccent", doneMarkerStyle: "elite", spinnerColor: "accent", spinnerColorEnabled: true, meterColor: "accent", meterColorEnabled: true, meterDimmed: false, tokenRateColor: "warning", tokenRateDimmed: false, promptIcon: true, promptTimestamp: true, promptProvider: true, promptModel: true, useNerdFont: true },
|
|
85
|
-
features: { substituteDefaultMessage: true, elapsedTime: true, outputTokens: true, tokenRate: true, doneMarker: true, doneMarkerIcon: true, randomizeDoneMarker: true, doneMarkerTokens: true, doneMarkerInputs: true },
|
|
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: "borderAccent", borderColorEnabled: true, borderStyle: "double", borderStyleEnabled: true, doneMarkerBorderStyle: "none", doneMarkerBorderColor: "borderAccent", doneMarkerStyle: "elite", spinnerColor: "accent", 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
|
+
features: { substituteDefaultMessage: true, elapsedTime: true, outputTokens: true, tokenRate: true, responseModel: true, doneMarker: true, doneMarkerIcon: true, randomizeDoneMarker: true, doneMarkerTokens: true, doneMarkerInputs: true },
|
|
86
89
|
loaderOrder: [...DEFAULT_LOADER_ORDER],
|
|
87
90
|
wordPacks: {},
|
|
88
91
|
};
|
|
@@ -96,6 +99,7 @@ const LOADER_ELEMENT_LABELS: Record<LoaderElement, string> = {
|
|
|
96
99
|
elapsed: "Elapsed time",
|
|
97
100
|
tokens: "Output tokens",
|
|
98
101
|
tokenRate: "Token rate",
|
|
102
|
+
responseModel: "Response model",
|
|
99
103
|
};
|
|
100
104
|
|
|
101
105
|
/**
|
|
@@ -198,6 +202,10 @@ export const MENU_ENTRIES: readonly MenuEntry[] = [
|
|
|
198
202
|
{ id: "showTokenRate", label: "Token rate", section: "“Working” Loader", group: "features", key: "tokenRate" },
|
|
199
203
|
{ id: "tokenRateColor", label: "Token rate color", section: "“Working” Loader", group: "decorations", key: "tokenRateColor", cycleValues: SETTING_COLOR_VALUES },
|
|
200
204
|
{ id: "tokenRateDimmed", label: "Token rate dimmed", section: "“Working” Loader", group: "decorations", key: "tokenRateDimmed" },
|
|
205
|
+
// id differs from key: the Elements Order row already owns "responseModel" in the menu's shared value namespace.
|
|
206
|
+
{ 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: SETTING_COLOR_VALUES },
|
|
208
|
+
{ id: "responseModelDimmed", label: "Response model dimmed", section: "“Working” Loader", group: "decorations", key: "responseModelDimmed" },
|
|
201
209
|
{ id: "doneMarker", label: "Show completion marker", section: "Completion Marker", group: "features", key: "doneMarker" },
|
|
202
210
|
{ id: "doneMarkerStyle", label: "Marker style", section: "Completion Marker", group: "decorations", key: "doneMarkerStyle", cycleValues: DONE_MARKER_STYLE_VALUES },
|
|
203
211
|
{ id: "doneMarkerBorderStyle", label: "Border style", section: "Completion Marker", group: "decorations", key: "doneMarkerBorderStyle", cycleValues: DONE_MARKER_BORDER_STYLE_VALUES },
|
|
@@ -228,6 +236,7 @@ function setDecorationCycleValue(decorations: DecorationSettings, key: keyof Dec
|
|
|
228
236
|
case "spinnerColor":
|
|
229
237
|
case "meterColor":
|
|
230
238
|
case "tokenRateColor":
|
|
239
|
+
case "responseModelColor":
|
|
231
240
|
case "doneMarkerBorderColor":
|
|
232
241
|
if (isSettingColor(stored)) decorations[key] = stored;
|
|
233
242
|
return;
|
package/src/util.ts
CHANGED
|
@@ -7,3 +7,12 @@ export function isPlainObject(value: unknown): value is Record<string, unknown>
|
|
|
7
7
|
export function stripControlChars(text: string): string {
|
|
8
8
|
return text.replace(/[\p{Cc}\p{Cf}]/gu, "");
|
|
9
9
|
}
|
|
10
|
+
|
|
11
|
+
/** True when a reported response model appears to be a decorated form of the selected model. */
|
|
12
|
+
export function modelsResemble(selectedModel?: string, responseModel?: string): boolean {
|
|
13
|
+
if (!selectedModel || !responseModel) return false;
|
|
14
|
+
const selectedName = selectedModel.split(/[\\/]/).at(-1)?.toLowerCase() ?? "";
|
|
15
|
+
const selectedTokens = selectedName.match(/[a-z0-9]{2,}/g) ?? [];
|
|
16
|
+
const normalizedResponse = responseModel.toLowerCase();
|
|
17
|
+
return selectedTokens.length > 0 && selectedTokens.every((token) => normalizedResponse.includes(token));
|
|
18
|
+
}
|
package/src/word-packs.ts
CHANGED
|
@@ -84,7 +84,6 @@ export function loadBundledWordPacks(): WordPack[] {
|
|
|
84
84
|
packs.push(pack);
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
if (!packs.some((pack) => pack.id === "simcity")) throw new Error("Missing bundled SimCity word pack");
|
|
88
87
|
const bundledIds = new Set(packs.map((pack) => pack.id));
|
|
89
88
|
for (const id of RESERVED_BUNDLED_PACK_IDS) {
|
|
90
89
|
if (!bundledIds.has(id)) throw new Error(`Missing bundled word pack: ${id}`);
|
|
@@ -109,8 +108,10 @@ export function isWordPackEnabled(id: string, enabled: Record<string, boolean>):
|
|
|
109
108
|
}
|
|
110
109
|
|
|
111
110
|
export function selectWorkingTextSelection(enabled: Record<string, boolean>, packs: readonly WordPack[], fraction: number): WorkingTextSelection {
|
|
112
|
-
const
|
|
113
|
-
const
|
|
111
|
+
const enabledWords = packs.filter((pack) => isWordPackEnabled(pack.id, enabled)).flatMap((pack) => pack.words);
|
|
112
|
+
const total = WORDS.length + enabledWords.length;
|
|
113
|
+
const idx = Math.min(total - 1, Math.floor(fraction * total));
|
|
114
|
+
const entry = idx < WORDS.length ? WORDS[idx] : enabledWords[idx - WORDS.length];
|
|
114
115
|
return { text: `${entry.present_tense}…`, pastTense: entry.past_tense };
|
|
115
116
|
}
|
|
116
117
|
|