@underactive/pi-topping 0.3.1 → 0.5.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 +59 -17
- package/package.json +5 -5
- package/src/activity-meter.ts +11 -8
- package/src/format.ts +159 -47
- package/src/menu.ts +84 -23
- package/src/preview.ts +115 -32
- package/src/prompt-decorator.ts +85 -10
- package/src/session.ts +184 -58
- package/src/settings.ts +168 -38
package/src/session.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AgentSettledEvent,
|
|
3
3
|
AgentStartEvent,
|
|
4
|
+
CustomEntry,
|
|
4
5
|
ExtensionAPI,
|
|
5
6
|
ExtensionCommandContext,
|
|
6
7
|
ExtensionContext,
|
|
@@ -9,22 +10,33 @@ import type {
|
|
|
9
10
|
InputEventResult,
|
|
10
11
|
SessionShutdownEvent,
|
|
11
12
|
SessionStartEvent,
|
|
13
|
+
Theme,
|
|
12
14
|
} from "@earendil-works/pi-coding-agent";
|
|
13
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
15
|
+
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
14
16
|
import { ActivityMeter, rateToLevel, TokRateTracker } from "./activity-meter.ts";
|
|
15
17
|
import {
|
|
16
18
|
buildWorkingMessage,
|
|
19
|
+
DEFAULT_WORKING_WORD,
|
|
20
|
+
dimAttribute,
|
|
21
|
+
ELAPSED_INTERVAL_MS,
|
|
22
|
+
fadeThemeColorString,
|
|
17
23
|
formatElapsed,
|
|
24
|
+
formatTokenRate,
|
|
18
25
|
formatTokens,
|
|
19
26
|
isFullyDefaultAppearance,
|
|
27
|
+
METER_INTERVAL_MS,
|
|
28
|
+
SHIMMER_INTERVAL_MS,
|
|
20
29
|
shimmerString,
|
|
30
|
+
SPINNER_FRAME_MS,
|
|
21
31
|
SPINNER_FRAMES,
|
|
32
|
+
TOKEN_RATE_FADE_SHADE_COUNT,
|
|
33
|
+
TOKEN_RATE_PLACEHOLDER,
|
|
22
34
|
StreamingWordCounter,
|
|
23
35
|
} from "./format.ts";
|
|
24
36
|
import { showMenu } from "./menu.ts";
|
|
25
|
-
import { applyMenuResult, buildMenuSections, loadSettings, saveSettings } from "./settings.ts";
|
|
37
|
+
import { applyMenuResult, buildMenuSections, loadSettings, saveSettings, type SettingColor } from "./settings.ts";
|
|
26
38
|
import { PreviewRenderer } from "./preview.ts";
|
|
27
|
-
import { PROMPT_BOX_TYPE, promptBoxRenderer, type PromptBoxDetails } from "./prompt-decorator.ts";
|
|
39
|
+
import { buildCompletionMarkerContent, buildCompletionMarkerLine, PROMPT_BOX_TYPE, promptBoxRenderer, stripControlChars, type PromptBoxDetails } from "./prompt-decorator.ts";
|
|
28
40
|
import { pickRandomWord, pickRawWord } from "./words.ts";
|
|
29
41
|
|
|
30
42
|
type MessageStartEvent = Extract<ExtensionEvent, { type: "message_start" }>;
|
|
@@ -33,18 +45,18 @@ type MessageEndEvent = Extract<ExtensionEvent, { type: "message_end" }>;
|
|
|
33
45
|
type ToolExecutionStartEvent = Extract<ExtensionEvent, { type: "tool_execution_start" }>;
|
|
34
46
|
|
|
35
47
|
const DONE_ENTRY_TYPE = "pi-topping-done";
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
39
|
-
const ELAPSED_INTERVAL_MS = 1_000;
|
|
48
|
+
// Reconciliation resets the EMA at message boundaries, so retain and then fade the last rate to avoid flicker.
|
|
49
|
+
const TOKEN_RATE_HOLD_MS = 1_500;
|
|
50
|
+
const TOKEN_RATE_FADE_MS = 250;
|
|
40
51
|
|
|
41
|
-
|
|
52
|
+
interface DoneEntryData {
|
|
42
53
|
word: string;
|
|
43
54
|
elapsedMs: number;
|
|
44
|
-
tokens
|
|
55
|
+
tokens?: number;
|
|
56
|
+
midTurnInputs?: number;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
|
-
|
|
59
|
+
interface SessionState {
|
|
48
60
|
startTime: number;
|
|
49
61
|
currentWord: string;
|
|
50
62
|
confirmTokens: number;
|
|
@@ -52,12 +64,30 @@ export interface SessionState {
|
|
|
52
64
|
shimmerOrigin: number;
|
|
53
65
|
activityMeter: ActivityMeter;
|
|
54
66
|
rateTracker: TokRateTracker;
|
|
55
|
-
|
|
67
|
+
lastTokenRateSampledAt: number;
|
|
68
|
+
lastTokenRateTotal: number;
|
|
69
|
+
tokenRateText: string;
|
|
70
|
+
tokenRateFadeStartsAt: number;
|
|
56
71
|
timer: ReturnType<typeof setInterval> | null;
|
|
57
72
|
busy: boolean;
|
|
73
|
+
midTurnInputs: number;
|
|
74
|
+
/** Last string passed to setWorkingMessage(), or the NOT_SENT sentinel before the first call. */
|
|
75
|
+
lastMessage: string | undefined | typeof NOT_SENT;
|
|
58
76
|
}
|
|
59
77
|
|
|
60
|
-
|
|
78
|
+
/** Sentinel distinguishing "never called setWorkingMessage()" from an explicit `undefined` message. */
|
|
79
|
+
const NOT_SENT = Symbol("not-sent");
|
|
80
|
+
|
|
81
|
+
/** Reset all token-rate tracking fields on `state` to their fresh-turn values. */
|
|
82
|
+
function resetTokenRateState(state: SessionState): void {
|
|
83
|
+
state.rateTracker.reset();
|
|
84
|
+
state.lastTokenRateSampledAt = 0;
|
|
85
|
+
state.lastTokenRateTotal = 0;
|
|
86
|
+
state.tokenRateText = "";
|
|
87
|
+
state.tokenRateFadeStartsAt = 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeFreshState(): SessionState {
|
|
61
91
|
return {
|
|
62
92
|
startTime: 0,
|
|
63
93
|
currentWord: "",
|
|
@@ -66,9 +96,14 @@ export function makeFreshState(): SessionState {
|
|
|
66
96
|
shimmerOrigin: 0,
|
|
67
97
|
activityMeter: new ActivityMeter(),
|
|
68
98
|
rateTracker: new TokRateTracker(),
|
|
69
|
-
|
|
99
|
+
lastTokenRateSampledAt: 0,
|
|
100
|
+
lastTokenRateTotal: 0,
|
|
101
|
+
tokenRateText: "",
|
|
102
|
+
tokenRateFadeStartsAt: 0,
|
|
70
103
|
timer: null,
|
|
71
104
|
busy: false,
|
|
105
|
+
midTurnInputs: 0,
|
|
106
|
+
lastMessage: NOT_SENT,
|
|
72
107
|
};
|
|
73
108
|
}
|
|
74
109
|
|
|
@@ -87,7 +122,6 @@ export class SessionManager {
|
|
|
87
122
|
#onSessionStart = async (_e: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
88
123
|
this.stopTimer();
|
|
89
124
|
this.#counter.reset();
|
|
90
|
-
this.#currentCtx = null;
|
|
91
125
|
this.#state = makeFreshState();
|
|
92
126
|
this.#settings = loadSettings();
|
|
93
127
|
this.#state.activityMeter.setDirection(this.#settings.decorations.meterDirection);
|
|
@@ -99,7 +133,8 @@ export class SessionManager {
|
|
|
99
133
|
this.#currentCtx = ctx;
|
|
100
134
|
if (!this.usable(ctx)) return;
|
|
101
135
|
|
|
102
|
-
this.resetTurn(Date.now());
|
|
136
|
+
if (!event.streamingBehavior && !this.#state.busy) this.resetTurn(Date.now());
|
|
137
|
+
else if (event.streamingBehavior && event.source !== "extension") this.#state.midTurnInputs++;
|
|
103
138
|
if (this.#settings.decorations.decorateUserPrompt && this.shouldDecorate(event)) {
|
|
104
139
|
this.#pi.sendMessage<PromptBoxDetails>(
|
|
105
140
|
{
|
|
@@ -110,8 +145,13 @@ export class SessionManager {
|
|
|
110
145
|
submittedAt: Date.now(),
|
|
111
146
|
showIcon: this.#settings.decorations.promptIcon,
|
|
112
147
|
showTimestamp: this.#settings.decorations.promptTimestamp,
|
|
148
|
+
showProvider: this.#settings.decorations.promptProvider,
|
|
149
|
+
showModel: this.#settings.decorations.promptModel,
|
|
113
150
|
icon: this.#settings.decorations.useNerdFont ? "" : "π",
|
|
151
|
+
provider: ctx.model?.provider,
|
|
152
|
+
model: ctx.model?.id,
|
|
114
153
|
borderColor: this.#settings.decorations.borderColor,
|
|
154
|
+
borderStyle: this.#settings.decorations.borderStyle,
|
|
115
155
|
},
|
|
116
156
|
},
|
|
117
157
|
{
|
|
@@ -159,8 +199,11 @@ export class SessionManager {
|
|
|
159
199
|
const exactTokens = event.message.usage?.output;
|
|
160
200
|
this.#state.confirmTokens += exactTokens ?? this.#state.liveTokens;
|
|
161
201
|
if (exactTokens !== undefined) {
|
|
202
|
+
// Only reset the sampling state here, not tokenRateText/tokenRateFadeStartsAt:
|
|
203
|
+
// those must keep holding/fading across message boundaries to avoid flicker.
|
|
162
204
|
this.#state.rateTracker.reset();
|
|
163
|
-
this.#state.
|
|
205
|
+
this.#state.lastTokenRateSampledAt = 0;
|
|
206
|
+
this.#state.lastTokenRateTotal = this.#state.confirmTokens;
|
|
164
207
|
}
|
|
165
208
|
this.#state.liveTokens = 0;
|
|
166
209
|
this.#counter.reset();
|
|
@@ -188,13 +231,14 @@ export class SessionManager {
|
|
|
188
231
|
ctx.ui.setWorkingMessage();
|
|
189
232
|
this.applyIndicator(ctx);
|
|
190
233
|
this.#state.activityMeter.reset();
|
|
191
|
-
this.#state
|
|
192
|
-
this.#state.
|
|
234
|
+
resetTokenRateState(this.#state);
|
|
235
|
+
this.#state.lastMessage = NOT_SENT;
|
|
193
236
|
if (this.#settings.features.doneMarker && hadPrompt) {
|
|
194
237
|
this.#pi.appendEntry<DoneEntryData>(DONE_ENTRY_TYPE, {
|
|
195
238
|
word: this.#settings.features.randomizeDoneMarker ? pickRawWord().past_tense : "Worked",
|
|
196
239
|
elapsedMs,
|
|
197
240
|
tokens: this.#state.confirmTokens,
|
|
241
|
+
midTurnInputs: this.#state.midTurnInputs,
|
|
198
242
|
});
|
|
199
243
|
}
|
|
200
244
|
this.#currentCtx = null;
|
|
@@ -204,6 +248,39 @@ export class SessionManager {
|
|
|
204
248
|
this.stopTimer();
|
|
205
249
|
};
|
|
206
250
|
|
|
251
|
+
#renderDoneEntry(entry: CustomEntry<DoneEntryData>, theme: Theme): Component | undefined {
|
|
252
|
+
if (!entry.data) return undefined;
|
|
253
|
+
const word = typeof entry.data.word === "string" ? stripControlChars(entry.data.word) : "Worked";
|
|
254
|
+
const elapsedMs = typeof entry.data.elapsedMs === "number" && Number.isFinite(entry.data.elapsedMs) ? entry.data.elapsedMs : 0;
|
|
255
|
+
const features = this.#settings.features;
|
|
256
|
+
const details: string[] = [];
|
|
257
|
+
if (features.doneMarkerTokens && typeof entry.data.tokens === "number") details.push(`↓ ${formatTokens(entry.data.tokens)} tokens`);
|
|
258
|
+
if (features.doneMarkerInputs && typeof entry.data.midTurnInputs === "number" && entry.data.midTurnInputs) {
|
|
259
|
+
details.push(`${entry.data.midTurnInputs} mid-turn input${entry.data.midTurnInputs === 1 ? "" : "s"}`);
|
|
260
|
+
}
|
|
261
|
+
const icon = features.doneMarkerIcon ? theme.fg("text", this.#settings.decorations.useNerdFont ? "" : "π") : "";
|
|
262
|
+
const markerContent = buildCompletionMarkerContent(theme, icon, word, formatElapsed(elapsedMs), details);
|
|
263
|
+
const borderStyle = this.#settings.decorations.doneMarkerBorderStyle;
|
|
264
|
+
const borderColor = this.#settings.decorations.doneMarkerBorderColor;
|
|
265
|
+
if (borderStyle === "none") return new Text(markerContent);
|
|
266
|
+
|
|
267
|
+
let cachedWidth: number | undefined;
|
|
268
|
+
let cachedLines: string[] | undefined;
|
|
269
|
+
return {
|
|
270
|
+
render(width: number): string[] {
|
|
271
|
+
if (cachedWidth !== width) {
|
|
272
|
+
cachedWidth = width;
|
|
273
|
+
cachedLines = ["", ` ${buildCompletionMarkerLine(markerContent, width - 2, theme, borderStyle, borderColor)}`, ""];
|
|
274
|
+
}
|
|
275
|
+
return cachedLines!;
|
|
276
|
+
},
|
|
277
|
+
invalidate(): void {
|
|
278
|
+
cachedWidth = undefined;
|
|
279
|
+
cachedLines = undefined;
|
|
280
|
+
},
|
|
281
|
+
} satisfies Component;
|
|
282
|
+
}
|
|
283
|
+
|
|
207
284
|
install(): void {
|
|
208
285
|
this.#pi.on("session_start", this.#onSessionStart);
|
|
209
286
|
this.#pi.on("input", this.#onInput);
|
|
@@ -214,16 +291,10 @@ export class SessionManager {
|
|
|
214
291
|
this.#pi.on("tool_execution_start", this.#onToolExecutionStart);
|
|
215
292
|
this.#pi.on("agent_settled", this.#onAgentSettled);
|
|
216
293
|
this.#pi.on("session_shutdown", this.#onSessionShutdown);
|
|
217
|
-
this.#pi.registerEntryRenderer<DoneEntryData>(DONE_ENTRY_TYPE, (entry, _o, theme) =>
|
|
218
|
-
entry.data
|
|
219
|
-
? new Text(
|
|
220
|
-
`${this.#settings.features.doneMarkerIcon ? theme.fg("text", this.#settings.decorations.useNerdFont ? "" : "π") : ""}${theme.fg("dim", `${this.#settings.features.doneMarkerIcon ? " " : ""}${entry.data.word} for ${formatElapsed(entry.data.elapsedMs)}${this.#settings.features.doneMarkerTokens && entry.data.tokens !== undefined ? ` (↓ ${formatTokens(entry.data.tokens)} tokens)` : ""}`)}`,
|
|
221
|
-
)
|
|
222
|
-
: undefined,
|
|
223
|
-
);
|
|
294
|
+
this.#pi.registerEntryRenderer<DoneEntryData>(DONE_ENTRY_TYPE, (entry, _o, theme) => this.#renderDoneEntry(entry, theme));
|
|
224
295
|
this.#pi.registerMessageRenderer<PromptBoxDetails>(PROMPT_BOX_TYPE, promptBoxRenderer);
|
|
225
296
|
this.#pi.registerCommand("topping-settings", {
|
|
226
|
-
description: "Configure
|
|
297
|
+
description: "Configure prompt decoration, working-loader features/order, and completion-marker settings.",
|
|
227
298
|
handler: async (_a, ctx) => this.showSettings(ctx),
|
|
228
299
|
});
|
|
229
300
|
}
|
|
@@ -236,14 +307,31 @@ export class SessionManager {
|
|
|
236
307
|
return event.source !== "extension" && !!event.text.trim() && !event.images?.length && !/^[\/!?:]/.test(event.text);
|
|
237
308
|
}
|
|
238
309
|
|
|
239
|
-
|
|
310
|
+
/**
|
|
311
|
+
* Pi's Loader always prepends its indicator to the working message, so a spinner
|
|
312
|
+
* that is not the leading element has to be drawn inside the message instead.
|
|
313
|
+
*/
|
|
314
|
+
private spinnerInMessage(): boolean {
|
|
315
|
+
return this.#settings.decorations.animatedSpinner && this.#settings.loaderOrder[0] !== "spinner";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private spinnerColor(): SettingColor {
|
|
240
319
|
const decorations = this.#settings.decorations;
|
|
241
|
-
|
|
320
|
+
return decorations.spinnerColorEnabled ? decorations.spinnerColor : "accent";
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private indicatorFingerprint(): string {
|
|
324
|
+
const decorations = this.#settings.decorations;
|
|
325
|
+
return `${decorations.animatedSpinner}:${decorations.spinnerColor}:${decorations.spinnerColorEnabled}:${this.#settings.loaderOrder[0]}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private applyIndicator(ctx: ExtensionContext): void {
|
|
329
|
+
if (!this.#settings.decorations.animatedSpinner || this.spinnerInMessage()) {
|
|
242
330
|
ctx.ui.setWorkingIndicator({ frames: [] });
|
|
243
331
|
return;
|
|
244
332
|
}
|
|
245
333
|
|
|
246
|
-
const color =
|
|
334
|
+
const color = this.spinnerColor();
|
|
247
335
|
if (color === "accent") {
|
|
248
336
|
ctx.ui.setWorkingIndicator(undefined);
|
|
249
337
|
return;
|
|
@@ -263,13 +351,11 @@ export class SessionManager {
|
|
|
263
351
|
|
|
264
352
|
const features = this.#settings.features;
|
|
265
353
|
const decorations = this.#settings.decorations;
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
? ELAPSED_INTERVAL_MS
|
|
272
|
-
: undefined;
|
|
354
|
+
let interval: number | undefined;
|
|
355
|
+
if (decorations.shimmer) interval = SHIMMER_INTERVAL_MS;
|
|
356
|
+
else if (decorations.tokenActivityMonitor || features.outputTokens || features.tokenRate) interval = METER_INTERVAL_MS;
|
|
357
|
+
else if (features.elapsedTime) interval = ELAPSED_INTERVAL_MS;
|
|
358
|
+
if (this.spinnerInMessage()) interval = Math.min(interval ?? SPINNER_FRAME_MS, SPINNER_FRAME_MS);
|
|
273
359
|
if (interval) this.#state.timer = setInterval(() => this.tick(), interval);
|
|
274
360
|
}
|
|
275
361
|
|
|
@@ -281,8 +367,9 @@ export class SessionManager {
|
|
|
281
367
|
state.confirmTokens = 0;
|
|
282
368
|
state.liveTokens = 0;
|
|
283
369
|
state.activityMeter.reset();
|
|
284
|
-
state
|
|
285
|
-
state.
|
|
370
|
+
resetTokenRateState(state);
|
|
371
|
+
state.midTurnInputs = 0;
|
|
372
|
+
state.lastMessage = NOT_SENT;
|
|
286
373
|
this.#counter.reset();
|
|
287
374
|
}
|
|
288
375
|
|
|
@@ -295,18 +382,32 @@ export class SessionManager {
|
|
|
295
382
|
const total = state.confirmTokens + state.liveTokens;
|
|
296
383
|
const features = this.#settings.features;
|
|
297
384
|
const decorations = this.#settings.decorations;
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
385
|
+
let hasNewTokenCount = false;
|
|
386
|
+
if ((decorations.tokenActivityMonitor || features.tokenRate) && now - state.lastTokenRateSampledAt >= METER_INTERVAL_MS) {
|
|
387
|
+
// Do not reset the fade for an EMA-only decay while output is quiet.
|
|
388
|
+
hasNewTokenCount = total > state.lastTokenRateTotal;
|
|
389
|
+
state.lastTokenRateTotal = total;
|
|
390
|
+
const tokenRate = state.rateTracker.sample(total, now);
|
|
391
|
+
if (decorations.tokenActivityMonitor) state.activityMeter.push(rateToLevel(tokenRate));
|
|
392
|
+
state.lastTokenRateSampledAt = now;
|
|
301
393
|
}
|
|
394
|
+
const spinner = this.spinnerInMessage()
|
|
395
|
+
? ctx.ui.theme.fg(this.spinnerColor(), SPINNER_FRAMES[Math.floor(now / SPINNER_FRAME_MS) % SPINNER_FRAMES.length]!)
|
|
396
|
+
: "";
|
|
302
397
|
if (isFullyDefaultAppearance(features, decorations)) {
|
|
303
|
-
|
|
398
|
+
const msg = spinner
|
|
399
|
+
? buildWorkingMessage(ctx.ui.theme, { spinner, text: ctx.ui.theme.fg("dim", DEFAULT_WORKING_WORD) }, this.#settings.loaderOrder)
|
|
400
|
+
: undefined;
|
|
401
|
+
if (msg !== state.lastMessage) {
|
|
402
|
+
state.lastMessage = msg;
|
|
403
|
+
ctx.ui.setWorkingMessage(msg);
|
|
404
|
+
}
|
|
304
405
|
return;
|
|
305
406
|
}
|
|
306
407
|
|
|
307
408
|
const word = features.substituteDefaultMessage ? state.currentWord : DEFAULT_WORKING_WORD;
|
|
308
409
|
const styled = decorations.shimmer
|
|
309
|
-
? shimmerString(word, now - state.shimmerOrigin, ctx.ui.theme, decorations.shimmerDirection)
|
|
410
|
+
? shimmerString(word, now - state.shimmerOrigin, ctx.ui.theme, decorations.shimmerDirection, decorations.shimmerSpeed, decorations.shimmerInverted)
|
|
310
411
|
: ctx.ui.theme.fg("text", word);
|
|
311
412
|
const meter = decorations.tokenActivityMonitor
|
|
312
413
|
? state.activityMeter.render((level, char) =>
|
|
@@ -319,16 +420,46 @@ export class SessionManager {
|
|
|
319
420
|
),
|
|
320
421
|
)
|
|
321
422
|
: "";
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
423
|
+
let tokenRateText = "";
|
|
424
|
+
if (features.tokenRate) {
|
|
425
|
+
const latestTokenRate = formatTokenRate(state.rateTracker.tokenRate);
|
|
426
|
+
if (latestTokenRate && hasNewTokenCount) {
|
|
427
|
+
state.tokenRateText = latestTokenRate;
|
|
428
|
+
state.tokenRateFadeStartsAt = now + TOKEN_RATE_HOLD_MS;
|
|
429
|
+
} else if (now >= state.tokenRateFadeStartsAt + TOKEN_RATE_FADE_MS) {
|
|
430
|
+
state.tokenRateText = "";
|
|
431
|
+
}
|
|
432
|
+
tokenRateText = state.tokenRateText;
|
|
433
|
+
}
|
|
434
|
+
const tokenRateSegment = !features.tokenRate
|
|
435
|
+
? ""
|
|
436
|
+
: !tokenRateText
|
|
437
|
+
? ctx.ui.theme.fg("dim", TOKEN_RATE_PLACEHOLDER)
|
|
438
|
+
: now < state.tokenRateFadeStartsAt
|
|
439
|
+
? ctx.ui.theme.fg(decorations.tokenRateColor, tokenRateText)
|
|
440
|
+
: fadeThemeColorString(
|
|
441
|
+
tokenRateText,
|
|
442
|
+
Math.floor((now - state.tokenRateFadeStartsAt) / (TOKEN_RATE_FADE_MS / TOKEN_RATE_FADE_SHADE_COUNT)),
|
|
443
|
+
ctx.ui.theme,
|
|
444
|
+
decorations.tokenRateColor,
|
|
445
|
+
);
|
|
446
|
+
const tokenRateStyled = tokenRateSegment && decorations.tokenRateDimmed ? dimAttribute(tokenRateSegment) : tokenRateSegment;
|
|
447
|
+
const msg = buildWorkingMessage(
|
|
448
|
+
ctx.ui.theme,
|
|
449
|
+
{
|
|
450
|
+
spinner,
|
|
451
|
+
text: styled,
|
|
326
452
|
meter,
|
|
327
|
-
formatElapsed(now - state.startTime),
|
|
328
|
-
formatTokens(total),
|
|
329
|
-
|
|
330
|
-
|
|
453
|
+
elapsed: features.elapsedTime ? formatElapsed(now - state.startTime) : "",
|
|
454
|
+
tokens: features.outputTokens ? `↓ ${formatTokens(total)} tokens` : "",
|
|
455
|
+
tokenRate: tokenRateStyled,
|
|
456
|
+
},
|
|
457
|
+
this.#settings.loaderOrder,
|
|
331
458
|
);
|
|
459
|
+
if (msg !== state.lastMessage) {
|
|
460
|
+
state.lastMessage = msg;
|
|
461
|
+
ctx.ui.setWorkingMessage(msg);
|
|
462
|
+
}
|
|
332
463
|
}
|
|
333
464
|
|
|
334
465
|
private async showSettings(ctx: ExtensionCommandContext): Promise<void> {
|
|
@@ -337,7 +468,7 @@ export class SessionManager {
|
|
|
337
468
|
return;
|
|
338
469
|
}
|
|
339
470
|
|
|
340
|
-
const before =
|
|
471
|
+
const before = this.indicatorFingerprint();
|
|
341
472
|
const preview = new PreviewRenderer(ctx);
|
|
342
473
|
const result = await showMenu<Record<string, boolean | string>>(ctx, {
|
|
343
474
|
title: "Pi Topping: Settings",
|
|
@@ -356,12 +487,7 @@ export class SessionManager {
|
|
|
356
487
|
}
|
|
357
488
|
|
|
358
489
|
this.#settings = updatedSettings;
|
|
359
|
-
if (
|
|
360
|
-
before !==
|
|
361
|
-
`${this.#settings.decorations.animatedSpinner}:${this.#settings.decorations.spinnerColor}:${this.#settings.decorations.spinnerColorEnabled}`
|
|
362
|
-
) {
|
|
363
|
-
this.applyIndicator(ctx);
|
|
364
|
-
}
|
|
490
|
+
if (before !== this.indicatorFingerprint()) this.applyIndicator(ctx);
|
|
365
491
|
this.#state.activityMeter.setDirection(this.#settings.decorations.meterDirection);
|
|
366
492
|
if (this.#state.busy) {
|
|
367
493
|
this.stopTimer();
|