@underactive/pi-topping 0.6.3 → 0.6.4
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 +14 -8
- package/package.json +1 -1
- package/src/format.ts +13 -7
- package/src/preview.ts +9 -4
- package/src/session.ts +117 -26
- package/src/settings.ts +11 -2
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 (with direction, speed, and invert option), token activity monitor (with color, direction, and dim toggle), elapsed timer, output token display,
|
|
9
|
+
**“Working” Loader** — animated spinner (with color choice), randomized activity word, optional word packs, shimmer (with direction, speed, and invert option), token activity monitor (with color, direction, and dim toggle), elapsed timer, output token display, live output-token rate (with color choice and dim toggle), and the response model (with visibility, color, and dim controls) — 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
|
|
|
@@ -23,7 +23,7 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
23
23
|
╔═[ Pi Topping: Settings ]═══════════════════════════════════╗
|
|
24
24
|
╟─ Preview ──────────────────────────────────────────────────╢
|
|
25
25
|
║ ║
|
|
26
|
-
║ ⠋ Crafting… ⣠⣤⣶⣶⣤⣠⣀⢀ 28
|
|
26
|
+
║ ⠋ Crafting… ⣠⣤⣶⣶⣤⣠⣀⢀ 28 tps · 3s · ↓ 84 tokens · test-model ║
|
|
27
27
|
║ ║
|
|
28
28
|
╟─ User Prompt ──────────────────────────────────────────────╢
|
|
29
29
|
║ [■] High-vis prompt ON ║
|
|
@@ -51,6 +51,9 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
51
51
|
║ [■] Token rate ON ║
|
|
52
52
|
║ [■] Token rate color ‹ warning › ║
|
|
53
53
|
║ [■] Token rate dimmed OFF ║
|
|
54
|
+
║ [■] Response model ON ║
|
|
55
|
+
║ [■] Response model color ‹ accent › ║
|
|
56
|
+
║ [■] Response model dimmed OFF ║
|
|
54
57
|
║ ║
|
|
55
58
|
╟─ Elements Order ───────────────────────────────────────────╢
|
|
56
59
|
║ [ ] Animated spinner ║
|
|
@@ -59,6 +62,7 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
59
62
|
║ [ ] Token rate ║
|
|
60
63
|
║ [ ] Elapsed time ║
|
|
61
64
|
║ [ ] Output tokens ║
|
|
65
|
+
║ [ ] Response model ║
|
|
62
66
|
║ ║
|
|
63
67
|
╟─ Completion Marker ────────────────────────────────────────╢
|
|
64
68
|
║ [■] Show completion marker ON ║
|
|
@@ -84,9 +88,9 @@ Run `/topping-settings` (TUI only) to customize your toppings. Settings persist
|
|
|
84
88
|
╟─ Options ──────────────────────────────────────────────────╢
|
|
85
89
|
║ [■] Use NerdFont icons ON ║
|
|
86
90
|
╟────────────────────────────────────────────────────────────╢
|
|
87
|
-
║ ↑↓ move PgUp/PgDn page ←→ select
|
|
88
|
-
║ ␣ toggle ⏎ apply esc cancel
|
|
89
|
-
╚════════════════════════════════════════════════════[ 9/
|
|
91
|
+
║ ↑↓ move PgUp/PgDn page ←→ select ║
|
|
92
|
+
║ ␣ toggle ⏎ apply esc cancel ║
|
|
93
|
+
╚════════════════════════════════════════════════════[ 9/50 ]╝
|
|
90
94
|
```
|
|
91
95
|
|
|
92
96
|
### Word packs
|
|
@@ -108,17 +112,19 @@ Bundled packs live in [`wordpacks/`](wordpacks/). Copy `wordpacks/doctor-who.jso
|
|
|
108
112
|
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
113
|
|
|
110
114
|
Under **Elements Order**, press `␣` to grab a row, then `↑`/`↓` to slide that element
|
|
111
|
-
left or right within the loader. Elapsed time, output tokens,
|
|
115
|
+
left or right within the loader. Elapsed time, output tokens, token rate, and response model are joined with
|
|
112
116
|
`·` separators whenever adjacent. When reordering separates them, each adjacent run is rendered
|
|
113
117
|
without a separator before or after it.
|
|
114
118
|
|
|
115
|
-
The `N
|
|
119
|
+
The `N tps` token rate uses the selected theme color and
|
|
116
120
|
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 `---
|
|
121
|
+
segments; larger rates may shift later segments. When inactive, it remains as the dim `--- tps` placeholder. After its last update it holds
|
|
118
122
|
full brightness for 1.5 seconds, then fades through five theme-aware shades to the dim text color over
|
|
119
123
|
the next 0.25 seconds before returning to the placeholder; a new count restores full brightness and
|
|
120
124
|
restarts the cycle.
|
|
121
125
|
|
|
126
|
+
The response model is captured from assistant responses, displayed as its sanitized value with no label, and defaults to the final loader detail after output tokens. 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.
|
|
127
|
+
|
|
122
128
|
## Install
|
|
123
129
|
|
|
124
130
|
```bash
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@underactive/pi-topping",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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,
|
|
@@ -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,10 @@ interface SessionState {
|
|
|
82
85
|
rateTracker: TokRateTracker;
|
|
83
86
|
lastTokenRateSampledAt: number;
|
|
84
87
|
lastTokenRateTotal: number;
|
|
88
|
+
responseModel: string;
|
|
89
|
+
responseModelHoldTimer: ReturnType<typeof setTimeout> | null;
|
|
90
|
+
responseModelFadeTimer: ReturnType<typeof setInterval> | null;
|
|
91
|
+
responseModelFadeGeneration: number;
|
|
85
92
|
tokenRateText: string;
|
|
86
93
|
tokenRateFadeStartsAt: number;
|
|
87
94
|
timer: ReturnType<typeof setInterval> | null;
|
|
@@ -115,6 +122,10 @@ function makeFreshState(): SessionState {
|
|
|
115
122
|
rateTracker: new TokRateTracker(),
|
|
116
123
|
lastTokenRateSampledAt: 0,
|
|
117
124
|
lastTokenRateTotal: 0,
|
|
125
|
+
responseModel: "",
|
|
126
|
+
responseModelHoldTimer: null,
|
|
127
|
+
responseModelFadeTimer: null,
|
|
128
|
+
responseModelFadeGeneration: 0,
|
|
118
129
|
tokenRateText: "",
|
|
119
130
|
tokenRateFadeStartsAt: 0,
|
|
120
131
|
timer: null,
|
|
@@ -145,13 +156,14 @@ export class SessionManager {
|
|
|
145
156
|
}
|
|
146
157
|
|
|
147
158
|
#onSessionStart = async (_e: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
159
|
+
this.#currentCtx = ctx;
|
|
148
160
|
this.stopTimer();
|
|
161
|
+
this.cancelResponseModelFade(ctx);
|
|
149
162
|
this.#counter.reset();
|
|
150
163
|
this.#state = makeFreshState();
|
|
151
164
|
this.#settings = loadSettings();
|
|
152
165
|
this.#userPacks = loadUserWordPacks();
|
|
153
166
|
this.#state.activityMeter.setDirection(this.#settings.decorations.meterDirection);
|
|
154
|
-
this.#currentCtx = ctx;
|
|
155
167
|
if (this.usable(ctx)) {
|
|
156
168
|
this.applyIndicator(ctx);
|
|
157
169
|
if (isSiblingSetupEnabled()) notifyMissingToppingsOnce(this.#pi, ctx.ui);
|
|
@@ -165,32 +177,38 @@ export class SessionManager {
|
|
|
165
177
|
if (!event.streamingBehavior && !this.#state.busy) this.resetTurn(Date.now());
|
|
166
178
|
else if (event.streamingBehavior && event.source !== "extension") this.#state.midTurnInputs++;
|
|
167
179
|
if (this.#settings.decorations.decorateUserPrompt && this.shouldDecorate(event)) {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
try {
|
|
181
|
+
await this.#pi.sendMessage<PromptBoxDetails>(
|
|
182
|
+
{
|
|
183
|
+
customType: PROMPT_BOX_TYPE,
|
|
184
|
+
content: event.text,
|
|
185
|
+
display: true,
|
|
186
|
+
details: {
|
|
187
|
+
submittedAt: Date.now(),
|
|
188
|
+
showIcon: this.#settings.decorations.promptIcon,
|
|
189
|
+
showTimestamp: this.#settings.decorations.promptTimestamp,
|
|
190
|
+
showProvider: this.#settings.decorations.promptProvider,
|
|
191
|
+
showModel: this.#settings.decorations.promptModel,
|
|
192
|
+
icon: this.#settings.decorations.useNerdFont ? "" : "π",
|
|
193
|
+
provider: ctx.model?.provider,
|
|
194
|
+
model: ctx.model?.id,
|
|
195
|
+
borderColor: this.#settings.decorations.borderColor,
|
|
196
|
+
borderStyle: this.#settings.decorations.borderStyle,
|
|
197
|
+
},
|
|
184
198
|
},
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
199
|
+
{ triggerTurn: true },
|
|
200
|
+
);
|
|
201
|
+
return { action: "handled" };
|
|
202
|
+
} catch {
|
|
203
|
+
ctx.ui.notify("Failed to submit prompt", "error");
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
189
206
|
}
|
|
190
207
|
};
|
|
191
208
|
|
|
192
209
|
#onAgentStart = async (_e: AgentStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
193
210
|
this.#currentCtx = ctx;
|
|
211
|
+
this.cancelResponseModelFade(ctx);
|
|
194
212
|
if (!this.usable(ctx)) return;
|
|
195
213
|
if (!this.#state.startTime) this.resetTurn(Date.now());
|
|
196
214
|
this.#state.busy = true;
|
|
@@ -203,6 +221,7 @@ export class SessionManager {
|
|
|
203
221
|
if (this.usable(ctx) && event.message.role === "assistant") {
|
|
204
222
|
this.#state.liveTokens = 0;
|
|
205
223
|
this.#counter.reset();
|
|
224
|
+
this.updateResponseModel((event.message as { responseModel?: unknown }).responseModel);
|
|
206
225
|
}
|
|
207
226
|
};
|
|
208
227
|
|
|
@@ -216,13 +235,20 @@ export class SessionManager {
|
|
|
216
235
|
) {
|
|
217
236
|
this.#state.liveTokens += this.#counter.count(assistantEvent.delta, assistantEvent.type);
|
|
218
237
|
}
|
|
238
|
+
if (this.usable(ctx)) {
|
|
239
|
+
const rawResponseModel = (assistantEvent as { partial?: { responseModel?: unknown } } | undefined)?.partial?.responseModel
|
|
240
|
+
?? (event.message as { responseModel?: unknown } | undefined)?.responseModel;
|
|
241
|
+
this.updateResponseModel(rawResponseModel);
|
|
242
|
+
}
|
|
219
243
|
};
|
|
220
244
|
|
|
221
245
|
#onMessageEnd = async (event: MessageEndEvent, ctx: ExtensionContext): Promise<void> => {
|
|
222
246
|
this.#currentCtx = ctx;
|
|
223
247
|
if (!this.usable(ctx) || event.message.role !== "assistant") return;
|
|
224
248
|
|
|
225
|
-
const
|
|
249
|
+
const rawTokens = event.message.usage?.output;
|
|
250
|
+
const exactTokens =
|
|
251
|
+
typeof rawTokens === "number" && Number.isFinite(rawTokens) && rawTokens >= 0 ? rawTokens : undefined;
|
|
226
252
|
this.#state.confirmTokens += exactTokens ?? this.#state.liveTokens;
|
|
227
253
|
if (exactTokens !== undefined) {
|
|
228
254
|
// Only reset the sampling state here, not tokenRateText/tokenRateFadeStartsAt:
|
|
@@ -233,6 +259,7 @@ export class SessionManager {
|
|
|
233
259
|
}
|
|
234
260
|
this.#state.liveTokens = 0;
|
|
235
261
|
this.#counter.reset();
|
|
262
|
+
this.updateResponseModel((event.message as { responseModel?: unknown }).responseModel);
|
|
236
263
|
};
|
|
237
264
|
|
|
238
265
|
#onToolExecutionStart = async (_e: ToolExecutionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
@@ -267,13 +294,17 @@ export class SessionManager {
|
|
|
267
294
|
|
|
268
295
|
const hadPrompt = !!this.#state.startTime;
|
|
269
296
|
const elapsedMs = hadPrompt ? Date.now() - this.#state.startTime : 0;
|
|
297
|
+
const responseModel = this.#settings.features.responseModel ? this.#state.responseModel : "";
|
|
298
|
+
const responseModelColor = this.#settings.decorations.responseModelColor;
|
|
299
|
+
const responseModelDimmed = this.#settings.decorations.responseModelDimmed;
|
|
270
300
|
this.#state.busy = false;
|
|
271
301
|
this.#state.waiting = null;
|
|
272
302
|
this.#state.startTime = 0;
|
|
273
303
|
this.#counter.reset();
|
|
274
304
|
this.stopTimer();
|
|
275
|
-
ctx.ui.setWorkingMessage();
|
|
276
305
|
this.applyIndicator(ctx);
|
|
306
|
+
ctx.ui.setWorkingMessage();
|
|
307
|
+
if (responseModel) this.startResponseModelFade(ctx, responseModel, responseModelColor, responseModelDimmed);
|
|
277
308
|
this.#state.activityMeter.reset();
|
|
278
309
|
resetTokenRateState(this.#state);
|
|
279
310
|
this.#state.lastMessage = NOT_SENT;
|
|
@@ -288,8 +319,9 @@ export class SessionManager {
|
|
|
288
319
|
this.#currentCtx = null;
|
|
289
320
|
};
|
|
290
321
|
|
|
291
|
-
#onSessionShutdown = async (_e: SessionShutdownEvent,
|
|
322
|
+
#onSessionShutdown = async (_e: SessionShutdownEvent, ctx: ExtensionContext): Promise<void> => {
|
|
292
323
|
this.#state.waiting = null;
|
|
324
|
+
this.cancelResponseModelFade(ctx);
|
|
293
325
|
this.stopTimer();
|
|
294
326
|
};
|
|
295
327
|
|
|
@@ -413,6 +445,58 @@ export class SessionManager {
|
|
|
413
445
|
}
|
|
414
446
|
}
|
|
415
447
|
|
|
448
|
+
private updateResponseModel(raw: unknown): void {
|
|
449
|
+
const responseModel = typeof raw === "string" ? stripControlChars(raw).trim() : "";
|
|
450
|
+
if (responseModel && responseModel !== this.#state.responseModel) {
|
|
451
|
+
this.#state.responseModel = responseModel;
|
|
452
|
+
this.tick();
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private cancelResponseModelFade(ctx?: ExtensionContext | null): void {
|
|
457
|
+
const state = this.#state;
|
|
458
|
+
state.responseModelFadeGeneration++;
|
|
459
|
+
if (state.responseModelHoldTimer) {
|
|
460
|
+
clearTimeout(state.responseModelHoldTimer);
|
|
461
|
+
state.responseModelHoldTimer = null;
|
|
462
|
+
}
|
|
463
|
+
if (state.responseModelFadeTimer) {
|
|
464
|
+
clearInterval(state.responseModelFadeTimer);
|
|
465
|
+
state.responseModelFadeTimer = null;
|
|
466
|
+
}
|
|
467
|
+
const activeCtx = ctx ?? this.#currentCtx;
|
|
468
|
+
if (activeCtx && this.usable(activeCtx)) {
|
|
469
|
+
activeCtx.ui.setStatus(RESPONSE_MODEL_STATUS_KEY, undefined);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private startResponseModelFade(ctx: ExtensionContext, model: string, color: SettingColor, dimmed: boolean): void {
|
|
474
|
+
this.cancelResponseModelFade(ctx);
|
|
475
|
+
const generation = this.#state.responseModelFadeGeneration;
|
|
476
|
+
const render = (shade?: number): void => {
|
|
477
|
+
const colored = shade === undefined
|
|
478
|
+
? ctx.ui.theme.fg(color, model)
|
|
479
|
+
: fadeThemeColorString(model, shade, ctx.ui.theme, color);
|
|
480
|
+
const responseModel = dimmed ? dimAttribute(colored) : colored;
|
|
481
|
+
ctx.ui.setStatus(RESPONSE_MODEL_STATUS_KEY, responseModel);
|
|
482
|
+
};
|
|
483
|
+
render();
|
|
484
|
+
this.#state.responseModelHoldTimer = setTimeout(() => {
|
|
485
|
+
if (this.#state.responseModelFadeGeneration !== generation) return;
|
|
486
|
+
this.#state.responseModelHoldTimer = null;
|
|
487
|
+
let shade = 0;
|
|
488
|
+
render(shade++);
|
|
489
|
+
this.#state.responseModelFadeTimer = setInterval(() => {
|
|
490
|
+
if (this.#state.responseModelFadeGeneration !== generation) return;
|
|
491
|
+
if (shade >= TOKEN_RATE_FADE_SHADE_COUNT) {
|
|
492
|
+
this.cancelResponseModelFade(ctx);
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
render(shade++);
|
|
496
|
+
}, RESPONSE_MODEL_FADE_MS / TOKEN_RATE_FADE_SHADE_COUNT);
|
|
497
|
+
}, RESPONSE_MODEL_HOLD_MS);
|
|
498
|
+
}
|
|
499
|
+
|
|
416
500
|
private startTimer(): void {
|
|
417
501
|
if (this.#state.timer) return;
|
|
418
502
|
|
|
@@ -431,12 +515,14 @@ export class SessionManager {
|
|
|
431
515
|
}
|
|
432
516
|
|
|
433
517
|
private resetTurn(now: number): void {
|
|
518
|
+
this.cancelResponseModelFade();
|
|
434
519
|
const state = this.#state;
|
|
435
520
|
state.startTime = now;
|
|
436
521
|
state.shimmerOrigin = now;
|
|
437
522
|
state.workingText = this.pickWorkingWord();
|
|
438
523
|
state.confirmTokens = 0;
|
|
439
524
|
state.liveTokens = 0;
|
|
525
|
+
state.responseModel = "";
|
|
440
526
|
state.activityMeter.reset();
|
|
441
527
|
resetTokenRateState(state);
|
|
442
528
|
state.midTurnInputs = 0;
|
|
@@ -473,9 +559,13 @@ export class SessionManager {
|
|
|
473
559
|
const spinner = this.spinnerInMessage()
|
|
474
560
|
? ctx.ui.theme.fg(this.spinnerColor(), SPINNER_FRAMES[Math.floor(now / SPINNER_FRAME_MS) % SPINNER_FRAMES.length]!)
|
|
475
561
|
: "";
|
|
562
|
+
const responseModelColored = features.responseModel && state.responseModel
|
|
563
|
+
? ctx.ui.theme.fg(decorations.responseModelColor, state.responseModel)
|
|
564
|
+
: "";
|
|
565
|
+
const responseModel = responseModelColored && decorations.responseModelDimmed ? dimAttribute(responseModelColored) : responseModelColored;
|
|
476
566
|
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)
|
|
567
|
+
const msg = spinner || responseModel
|
|
568
|
+
? buildWorkingMessage(ctx.ui.theme, { spinner, text: ctx.ui.theme.fg("dim", DEFAULT_WORKING_WORD), responseModel }, this.#settings.loaderOrder)
|
|
479
569
|
: undefined;
|
|
480
570
|
if (msg !== state.lastMessage) {
|
|
481
571
|
state.lastMessage = msg;
|
|
@@ -532,6 +622,7 @@ export class SessionManager {
|
|
|
532
622
|
elapsed: features.elapsedTime ? formatElapsed(now - state.startTime) : "",
|
|
533
623
|
tokens: features.outputTokens ? `↓ ${formatTokens(total)} tokens` : "",
|
|
534
624
|
tokenRate: tokenRateStyled,
|
|
625
|
+
responseModel,
|
|
535
626
|
},
|
|
536
627
|
this.#settings.loaderOrder,
|
|
537
628
|
);
|
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;
|