@underactive/pi-topping 0.12.0 → 0.13.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 +1 -1
- package/package.json +7 -4
- package/src/format.ts +10 -201
- package/src/loader-message.ts +7 -5
- package/src/nvidia-green.ts +16 -13
- package/src/output-style.ts +0 -7
- package/src/pi-installer.ts +39 -56
- package/src/preview.ts +29 -12
- package/src/prompt-decorator.ts +17 -19
- package/src/session.ts +51 -81
- package/src/settings.ts +89 -83
- package/src/setup-check.ts +1 -1
- package/src/setup-command.ts +9 -4
- package/src/util.ts +3 -21
- package/src/activity-meter.ts +0 -147
- package/src/menu.ts +0 -697
- package/src/routesense-feed.ts +0 -49
- package/src/statusline-embed.ts +0 -12
package/src/prompt-decorator.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import type { EntryRenderer, MessageRenderer, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
3
|
+
import { thinkingLevelColorizer } from "@underactive/pi-topping-primitives/activity-tui";
|
|
4
|
+
import { BOX_STYLES, type BoxGlyphs } from "@underactive/pi-topping-primitives/glyphs";
|
|
5
|
+
import { isThinkingLevel, type ThinkingLevel } from "./format.ts";
|
|
4
6
|
import { DEFAULT_SETTINGS, isBorderStyle, isPromptBorderColor, type BorderStyle, type DoneMarkerBorderColor, type DoneMarkerBorderStyle, type DoneMarkerStyle, type PromptBorderColor } from "./settings.ts";
|
|
5
7
|
import { stripControlChars } from "./util.ts";
|
|
6
8
|
|
|
7
9
|
export const PROMPT_BOX_TYPE = "pi-topping-prompt";
|
|
8
10
|
export const PROMPT_BOX_ENTRY_TYPE = "pi-topping-prompt-entry";
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
single: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" },
|
|
13
|
-
rounded: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" },
|
|
14
|
-
heavy: { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" },
|
|
15
|
-
};
|
|
12
|
+
// Typed against this repo's BorderStyle, so a style added to settings.ts without a glyph set fails the typecheck.
|
|
13
|
+
const BORDER_GLYPHS: Readonly<Record<BorderStyle, BoxGlyphs>> = BOX_STYLES;
|
|
16
14
|
|
|
17
15
|
/**
|
|
18
16
|
* False visibility options hide each corresponding part; present provider and
|
|
@@ -79,12 +77,12 @@ export function buildCompletionMarkerLine(
|
|
|
79
77
|
if (borderStyle === "none") return truncateToWidth(content, safeWidth);
|
|
80
78
|
|
|
81
79
|
const g = BORDER_GLYPHS[borderStyle];
|
|
82
|
-
const border =
|
|
83
|
-
const prefix = `${g.
|
|
80
|
+
const border = thinkingLevelColorizer(theme, borderColor, thinkingLevel);
|
|
81
|
+
const prefix = `${g.bottomLeft}${g.horizontal.repeat(2)} `;
|
|
84
82
|
|
|
85
83
|
const suffix = markerStyle === "bookend"
|
|
86
|
-
? ` ${g.
|
|
87
|
-
: ` ${COMPLETION_MARKER_TRAIL_RUNS.map(length => g.
|
|
84
|
+
? ` ${g.horizontal.repeat(Math.max(0, safeWidth - visibleWidth(prefix) - visibleWidth(content) - 2))}${g.bottomRight}`
|
|
85
|
+
: ` ${COMPLETION_MARKER_TRAIL_RUNS.map(length => g.horizontal.repeat(length)).join(" ")}`;
|
|
88
86
|
const line = `${border(prefix)}${content}${border(suffix)}`;
|
|
89
87
|
return visibleWidth(line) > safeWidth ? truncateToWidth(line, safeWidth) : line;
|
|
90
88
|
}
|
|
@@ -103,7 +101,7 @@ export function buildPromptBoxLines(
|
|
|
103
101
|
const g = BORDER_GLYPHS[borderStyle];
|
|
104
102
|
const borderColor = isPromptBorderColor(options.borderColor) ? options.borderColor : DEFAULT_SETTINGS.decorations.borderColor;
|
|
105
103
|
const thinkingLevel = isThinkingLevel(options.thinkingLevel) ? options.thinkingLevel : undefined;
|
|
106
|
-
const border =
|
|
104
|
+
const border = thinkingLevelColorizer(theme, borderColor, thinkingLevel);
|
|
107
105
|
const iconColor = (text: string) => theme.fg("text", text);
|
|
108
106
|
const muted = (text: string) => theme.fg("dim", text);
|
|
109
107
|
const bodyText = (text: string) => theme.fg("customMessageText", text);
|
|
@@ -131,12 +129,12 @@ export function buildPromptBoxLines(
|
|
|
131
129
|
const labelText = combinedLabel
|
|
132
130
|
? truncateToWidth(combinedLabel, Math.max(0, innerWidth - 3)).replace(/\x1b\[0m/g, "")
|
|
133
131
|
: "";
|
|
134
|
-
const titleLength = icon ? visibleWidth(`${g.
|
|
132
|
+
const titleLength = icon ? visibleWidth(`${g.horizontal}${g.horizontal} ${icon} `) : 0;
|
|
135
133
|
const timeLength = time ? time.length + 3 : 0; // time + right-side " ═"
|
|
136
134
|
const topFill = Math.max(0, innerWidth - titleLength - timeLength);
|
|
137
|
-
const iconSeg = icon ? `${border(`${g.
|
|
138
|
-
const timeSeg = time ? `${border(" ")}${muted(time)}${border(` ${g.
|
|
139
|
-
const topLine = border(g.
|
|
135
|
+
const iconSeg = icon ? `${border(`${g.horizontal}${g.horizontal} `)}${iconColor(icon)}${border(" ")}` : "";
|
|
136
|
+
const timeSeg = time ? `${border(" ")}${muted(time)}${border(` ${g.horizontal}`)}` : "";
|
|
137
|
+
const topLine = border(g.topLeft) + iconSeg + border(g.horizontal.repeat(topFill)) + timeSeg + border(g.topRight);
|
|
140
138
|
const lines = [visibleWidth(topLine) > width ? truncateToWidth(topLine, width) : topLine];
|
|
141
139
|
|
|
142
140
|
if (content) {
|
|
@@ -144,14 +142,14 @@ export function buildPromptBoxLines(
|
|
|
144
142
|
for (const rawLine of wrapTextWithAnsi(content, textWidth)) {
|
|
145
143
|
const displayLine = rawLine || " ";
|
|
146
144
|
const padded = `${displayLine}${" ".repeat(Math.max(0, textWidth - visibleWidth(displayLine)))}`;
|
|
147
|
-
const line = `${border(g.
|
|
145
|
+
const line = `${border(g.vertical)}${bodyText(` ${padded} `)}${border(g.vertical)}`;
|
|
148
146
|
lines.push(visibleWidth(line) > width ? truncateToWidth(line, width) : line);
|
|
149
147
|
}
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
const labelLength = labelText ? visibleWidth(labelText) + 3 : 0; // label + right-side " ═"
|
|
153
|
-
const labelSeg = labelText ? `${border(" ")}${muted(labelText)}${border(` ${g.
|
|
154
|
-
const bottomLine = `${border(g.
|
|
151
|
+
const labelSeg = labelText ? `${border(" ")}${muted(labelText)}${border(` ${g.horizontal}`)}` : "";
|
|
152
|
+
const bottomLine = `${border(g.bottomLeft)}${border(g.horizontal.repeat(Math.max(0, innerWidth - labelLength)))}${labelSeg}${border(g.bottomRight)}`;
|
|
155
153
|
lines.push(visibleWidth(bottomLine) > width ? truncateToWidth(bottomLine, width) : bottomLine);
|
|
156
154
|
return lines;
|
|
157
155
|
}
|
package/src/session.ts
CHANGED
|
@@ -15,16 +15,26 @@ import type {
|
|
|
15
15
|
Theme,
|
|
16
16
|
} from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import { VERSION } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { CHANNELS, PI_VIRTUAL_MODEL_API, WORKING_STATUS_SLIDE_MS, subscribe } from "@underactive/pi-topping-primitives/contracts";
|
|
18
19
|
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
19
|
-
import {
|
|
20
|
-
|
|
20
|
+
import {
|
|
21
|
+
ActivityMeter,
|
|
22
|
+
FADE_SHADE_COUNT,
|
|
23
|
+
formatElapsedUnits,
|
|
24
|
+
RateHoldFade,
|
|
25
|
+
rateToLevel,
|
|
26
|
+
slideOutTail,
|
|
27
|
+
StreamingWordCounter,
|
|
28
|
+
} from "@underactive/pi-topping-primitives/activity";
|
|
21
29
|
import {
|
|
22
30
|
dimAttribute,
|
|
23
|
-
|
|
31
|
+
fadeThemeColorString,
|
|
32
|
+
renderMeter,
|
|
33
|
+
thinkingLevelColorizer,
|
|
34
|
+
} from "@underactive/pi-topping-primitives/activity-tui";
|
|
35
|
+
import {
|
|
24
36
|
isThinkingLevel,
|
|
25
37
|
ELAPSED_INTERVAL_MS,
|
|
26
|
-
fadeThemeColorString,
|
|
27
|
-
formatElapsed,
|
|
28
38
|
formatTokenRate,
|
|
29
39
|
formatTokens,
|
|
30
40
|
isFullyDefaultAppearance,
|
|
@@ -32,22 +42,17 @@ import {
|
|
|
32
42
|
RESPONSE_MODEL_FADE_MS,
|
|
33
43
|
RESPONSE_MODEL_HOLD_MS,
|
|
34
44
|
RESPONSE_MODEL_SLIDE_FRAME_MS,
|
|
35
|
-
RESPONSE_MODEL_SLIDE_MS,
|
|
36
45
|
SHIMMER_INTERVAL_MS,
|
|
37
|
-
slideOutTail,
|
|
38
46
|
SPINNER_FRAME_MS,
|
|
39
47
|
SPINNER_FRAMES,
|
|
40
|
-
TOKEN_RATE_FADE_SHADE_COUNT,
|
|
41
|
-
StreamingWordCounter,
|
|
42
48
|
type ThinkingLevel,
|
|
43
49
|
} from "./format.ts";
|
|
44
50
|
import { buildLoaderMessage } from "./loader-message.ts";
|
|
45
|
-
import { showMenu } from "
|
|
51
|
+
import { showMenu } from "@underactive/pi-topping-primitives/menu";
|
|
46
52
|
import { applyOutputStyle, OUTPUT_STYLES } from "./output-style.ts";
|
|
47
53
|
import { getResponseModelColorizer } from "./nvidia-green.ts";
|
|
48
54
|
import { registerSetupCommand } from "./setup-command.ts";
|
|
49
55
|
import { applyMenuResult, buildMenuSections, loadSettings, saveSettings, type ThinkingLevelColor } from "./settings.ts";
|
|
50
|
-
import { announcesEmbedded, STATUSLINE_EMBED_CHANNEL } from "./statusline-embed.ts";
|
|
51
56
|
import { announceMissingToppingsOnce, type MissingToppingsEntryData, renderMissingToppingsEntry, SETUP_ENTRY_TYPE } from "./toppings.ts";
|
|
52
57
|
import { PreviewRenderer } from "./preview.ts";
|
|
53
58
|
import {
|
|
@@ -81,8 +86,6 @@ type UIPromptKind = UIPromptStartEvent["kind"];
|
|
|
81
86
|
export const MIN_HOST_VERSION = "1.2.0";
|
|
82
87
|
const DONE_ENTRY_TYPE = "pi-topping-done";
|
|
83
88
|
const RESPONSE_MODEL_STATUS_KEY = "pi-topping-response-model";
|
|
84
|
-
/** Pi's `VIRTUAL_MODEL_API` for extension-registered virtual models; Pi does not export it from its public index. */
|
|
85
|
-
const VIRTUAL_MODEL_API = "pi-virtual";
|
|
86
89
|
const WAITING_LABELS: Record<UIPromptKind, string> = {
|
|
87
90
|
select: "Waiting for selection",
|
|
88
91
|
confirm: "Waiting for confirmation",
|
|
@@ -129,9 +132,7 @@ interface SessionState {
|
|
|
129
132
|
liveTokens: number;
|
|
130
133
|
shimmerOrigin: number;
|
|
131
134
|
activityMeter: ActivityMeter;
|
|
132
|
-
|
|
133
|
-
lastTokenRateSampledAt: number;
|
|
134
|
-
lastTokenRateTotal: number;
|
|
135
|
+
tokenRate: RateHoldFade;
|
|
135
136
|
responseModel: string;
|
|
136
137
|
served: { model: string; backend: string; routedId: string } | null;
|
|
137
138
|
/** The backend companion to the last raw response-model report. */
|
|
@@ -147,8 +148,6 @@ interface SessionState {
|
|
|
147
148
|
/** When the response model appeared in the loader and began sliding out; 0 if it never slid. */
|
|
148
149
|
responseModelShownAt: number;
|
|
149
150
|
responseModelSlideTimer: ReturnType<typeof setInterval> | null;
|
|
150
|
-
tokenRateText: string;
|
|
151
|
-
tokenRateFadeStartsAt: number;
|
|
152
151
|
timer: ReturnType<typeof setInterval> | null;
|
|
153
152
|
busy: boolean;
|
|
154
153
|
waiting: { kind: UIPromptKind; title?: string } | null;
|
|
@@ -160,15 +159,6 @@ interface SessionState {
|
|
|
160
159
|
/** Sentinel distinguishing "never called setWorkingMessage()" from an explicit `undefined` message. */
|
|
161
160
|
const NOT_SENT = Symbol("not-sent");
|
|
162
161
|
|
|
163
|
-
/** Reset all token-rate tracking fields on `state` to their fresh-turn values. */
|
|
164
|
-
function resetTokenRateState(state: SessionState): void {
|
|
165
|
-
state.rateTracker.reset();
|
|
166
|
-
state.lastTokenRateSampledAt = 0;
|
|
167
|
-
state.lastTokenRateTotal = 0;
|
|
168
|
-
state.tokenRateText = "";
|
|
169
|
-
state.tokenRateFadeStartsAt = 0;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
162
|
function makeFreshState(): SessionState {
|
|
173
163
|
return {
|
|
174
164
|
startTime: 0,
|
|
@@ -177,9 +167,12 @@ function makeFreshState(): SessionState {
|
|
|
177
167
|
liveTokens: 0,
|
|
178
168
|
shimmerOrigin: 0,
|
|
179
169
|
activityMeter: new ActivityMeter(),
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
170
|
+
tokenRate: new RateHoldFade({
|
|
171
|
+
holdMs: TOKEN_RATE_HOLD_MS,
|
|
172
|
+
fadeMs: TOKEN_RATE_FADE_MS,
|
|
173
|
+
shadeCount: FADE_SHADE_COUNT,
|
|
174
|
+
sampleIntervalMs: METER_INTERVAL_MS,
|
|
175
|
+
}),
|
|
183
176
|
responseModel: "",
|
|
184
177
|
served: null,
|
|
185
178
|
lastResponseModelBackend: NOT_SENT,
|
|
@@ -191,8 +184,6 @@ function makeFreshState(): SessionState {
|
|
|
191
184
|
responseModelFadeGeneration: 0,
|
|
192
185
|
responseModelShownAt: 0,
|
|
193
186
|
responseModelSlideTimer: null,
|
|
194
|
-
tokenRateText: "",
|
|
195
|
-
tokenRateFadeStartsAt: 0,
|
|
196
187
|
timer: null,
|
|
197
188
|
busy: false,
|
|
198
189
|
waiting: null,
|
|
@@ -334,11 +325,9 @@ export class SessionManager {
|
|
|
334
325
|
typeof rawTokens === "number" && Number.isFinite(rawTokens) && rawTokens >= 0 ? rawTokens : undefined;
|
|
335
326
|
this.#state.confirmTokens += exactTokens ?? this.#state.liveTokens;
|
|
336
327
|
if (exactTokens !== undefined) {
|
|
337
|
-
// Only
|
|
328
|
+
// Only restart the sampling here, not the held rate or its fade:
|
|
338
329
|
// those must keep holding/fading across message boundaries to avoid flicker.
|
|
339
|
-
this.#state.
|
|
340
|
-
this.#state.lastTokenRateSampledAt = 0;
|
|
341
|
-
this.#state.lastTokenRateTotal = this.#state.confirmTokens;
|
|
330
|
+
this.#state.tokenRate.resetSampling(this.#state.confirmTokens);
|
|
342
331
|
}
|
|
343
332
|
this.#state.liveTokens = 0;
|
|
344
333
|
this.#counter.reset();
|
|
@@ -407,7 +396,7 @@ export class SessionManager {
|
|
|
407
396
|
ctx.ui.setWorkingMessage();
|
|
408
397
|
if (responseModel) this.startResponseModelFade(ctx, responseModel, responseModelColor, responseModelDimmed);
|
|
409
398
|
this.#state.activityMeter.reset();
|
|
410
|
-
|
|
399
|
+
this.#state.tokenRate.reset();
|
|
411
400
|
this.#state.lastMessage = NOT_SENT;
|
|
412
401
|
if (this.#settings.features.doneMarker && hadPrompt) {
|
|
413
402
|
const aborted = e.aborted === true;
|
|
@@ -451,10 +440,10 @@ export class SessionManager {
|
|
|
451
440
|
const thinkingLevel = isThinkingLevel(entry.data.thinkingLevel) ? entry.data.thinkingLevel : undefined;
|
|
452
441
|
const rawModel = sanitizedText(entry.data.model);
|
|
453
442
|
const modelColored = features.doneMarkerModel && rawModel
|
|
454
|
-
?
|
|
443
|
+
? thinkingLevelColorizer(theme, this.#settings.decorations.doneMarkerModelColor, thinkingLevel)(rawModel)
|
|
455
444
|
: "";
|
|
456
445
|
const model = modelColored && this.#settings.decorations.doneMarkerModelDimmed ? dimAttribute(modelColored) : modelColored;
|
|
457
|
-
const markerContent = buildCompletionMarkerContent(theme, icon, word,
|
|
446
|
+
const markerContent = buildCompletionMarkerContent(theme, icon, word, formatElapsedUnits(elapsedMs), details, model, aborted ? "warning" : "dim");
|
|
458
447
|
const borderStyle = this.#settings.decorations.doneMarkerBorderStyle;
|
|
459
448
|
const borderColor = aborted ? "warning" : this.#settings.decorations.doneMarkerBorderColor;
|
|
460
449
|
const markerStyle = this.#settings.decorations.doneMarkerStyle;
|
|
@@ -491,14 +480,13 @@ export class SessionManager {
|
|
|
491
480
|
this.#pi.on("ui_prompt_start", this.#onUIPromptStart);
|
|
492
481
|
this.#pi.on("ui_prompt_end", this.#onUIPromptEnd);
|
|
493
482
|
this.#pi.on("session_shutdown", this.#onSessionShutdown);
|
|
494
|
-
this.#pi.events.
|
|
495
|
-
this.#loaderInStatusline =
|
|
483
|
+
subscribe(this.#pi.events, CHANNELS.workingStatusEmbedded, ({ embedded }) => {
|
|
484
|
+
this.#loaderInStatusline = embedded;
|
|
496
485
|
});
|
|
497
|
-
this.#pi.events.
|
|
486
|
+
subscribe(this.#pi.events, CHANNELS.routedModel, (announcement) => {
|
|
498
487
|
// Direct routes serve compaction and other extensions' side requests; a route while idle
|
|
499
488
|
// is such a background request too, and must not leave a stale model for the next turn.
|
|
500
|
-
|
|
501
|
-
if (!announcement || announcement.reason === "direct" || !this.#state.busy) return;
|
|
489
|
+
if (announcement.reason === "direct" || !this.#state.busy) return;
|
|
502
490
|
this.updateResponseModel({ raw: announcement.modelId, weak: true, provider: announcement.provider, routedId: announcement.modelId });
|
|
503
491
|
});
|
|
504
492
|
this.#pi.registerEntryRenderer<DoneEntryData>(DONE_ENTRY_TYPE, (entry, _o, theme) => this.#renderDoneEntry(entry, theme));
|
|
@@ -597,7 +585,7 @@ export class SessionManager {
|
|
|
597
585
|
|
|
598
586
|
const color = this.#settings.decorations.spinnerColor;
|
|
599
587
|
ctx.ui.setWorkingIndicator({
|
|
600
|
-
frames: SPINNER_FRAMES.map((frame) =>
|
|
588
|
+
frames: SPINNER_FRAMES.map((frame) => thinkingLevelColorizer(ctx.ui.theme, color, ctx.thinkingLevel)(frame)),
|
|
601
589
|
});
|
|
602
590
|
}
|
|
603
591
|
|
|
@@ -618,7 +606,7 @@ export class SessionManager {
|
|
|
618
606
|
if (message.responseModel !== undefined) {
|
|
619
607
|
return { raw: message.responseModel, weak: false, routedId: sanitizedText(message.model) || undefined };
|
|
620
608
|
}
|
|
621
|
-
if (this.#currentCtx?.model?.api !==
|
|
609
|
+
if (this.#currentCtx?.model?.api !== PI_VIRTUAL_MODEL_API) return undefined;
|
|
622
610
|
return { raw: message.model, weak: true, provider: typeof message.provider === "string" ? message.provider : undefined };
|
|
623
611
|
}
|
|
624
612
|
|
|
@@ -632,7 +620,7 @@ export class SessionManager {
|
|
|
632
620
|
private restatesSelection(report: ResponseModelReport, responseModel: string): boolean {
|
|
633
621
|
const selected = this.#currentCtx?.model;
|
|
634
622
|
if (!selected) return false;
|
|
635
|
-
if (selected.api ===
|
|
623
|
+
if (selected.api === PI_VIRTUAL_MODEL_API) return report.provider === selected.provider && responseModel === selected.id;
|
|
636
624
|
return modelsResemble(selected.id, responseModel) || modelsSpelledAlike(selected, responseModel);
|
|
637
625
|
}
|
|
638
626
|
|
|
@@ -684,7 +672,7 @@ export class SessionManager {
|
|
|
684
672
|
state.responseModelShownAt = Date.now();
|
|
685
673
|
state.responseModelSlideTimer = setInterval(() => {
|
|
686
674
|
this.tick();
|
|
687
|
-
if (Date.now() - state.responseModelShownAt >=
|
|
675
|
+
if (Date.now() - state.responseModelShownAt >= WORKING_STATUS_SLIDE_MS) this.stopResponseModelSlide();
|
|
688
676
|
}, RESPONSE_MODEL_SLIDE_FRAME_MS);
|
|
689
677
|
}
|
|
690
678
|
|
|
@@ -737,12 +725,12 @@ export class SessionManager {
|
|
|
737
725
|
render(shade++);
|
|
738
726
|
this.#state.responseModelFadeTimer = setInterval(() => {
|
|
739
727
|
if (this.#state.responseModelFadeGeneration !== generation) return;
|
|
740
|
-
if (shade >=
|
|
728
|
+
if (shade >= FADE_SHADE_COUNT) {
|
|
741
729
|
this.cancelResponseModelFade(ctx);
|
|
742
730
|
return;
|
|
743
731
|
}
|
|
744
732
|
render(shade++);
|
|
745
|
-
}, RESPONSE_MODEL_FADE_MS /
|
|
733
|
+
}, RESPONSE_MODEL_FADE_MS / FADE_SHADE_COUNT);
|
|
746
734
|
}, RESPONSE_MODEL_HOLD_MS);
|
|
747
735
|
}
|
|
748
736
|
|
|
@@ -779,7 +767,7 @@ export class SessionManager {
|
|
|
779
767
|
state.lastResponseModelRaw = NOT_SENT;
|
|
780
768
|
state.lastResponseModelBackend = NOT_SENT;
|
|
781
769
|
state.activityMeter.reset();
|
|
782
|
-
|
|
770
|
+
state.tokenRate.reset();
|
|
783
771
|
state.midTurnInputs = 0;
|
|
784
772
|
state.lastMessage = NOT_SENT;
|
|
785
773
|
this.#counter.reset();
|
|
@@ -802,27 +790,15 @@ export class SessionManager {
|
|
|
802
790
|
const total = state.confirmTokens + state.liveTokens;
|
|
803
791
|
const features = this.#settings.features;
|
|
804
792
|
const decorations = this.#settings.decorations;
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
state.lastTokenRateTotal = total;
|
|
810
|
-
const tokenRate = state.rateTracker.sample(total, now);
|
|
811
|
-
if (decorations.tokenActivityMonitor) state.activityMeter.push(rateToLevel(tokenRate, decorations.meterUpperBoundTps));
|
|
812
|
-
state.lastTokenRateSampledAt = now;
|
|
793
|
+
// RateHoldFade samples at most once per METER_INTERVAL_MS and holds a rate only when the
|
|
794
|
+
// total grew, so an EMA-only decay while output is quiet does not reset the fade.
|
|
795
|
+
if ((decorations.tokenActivityMonitor || features.tokenRate) && state.tokenRate.sample(total, now) && decorations.tokenActivityMonitor) {
|
|
796
|
+
state.activityMeter.push(rateToLevel(state.tokenRate.tracker.tokenRate, decorations.meterUpperBoundTps));
|
|
813
797
|
}
|
|
814
798
|
const responseModelShown = features.responseModel && state.responseModel
|
|
815
799
|
? slideOutTail(state.responseModel, now - state.responseModelShownAt)
|
|
816
800
|
: "";
|
|
817
|
-
|
|
818
|
-
const latestTokenRate = formatTokenRate(state.rateTracker.tokenRate);
|
|
819
|
-
if (latestTokenRate && hasNewTokenCount) {
|
|
820
|
-
state.tokenRateText = latestTokenRate;
|
|
821
|
-
state.tokenRateFadeStartsAt = now + TOKEN_RATE_HOLD_MS;
|
|
822
|
-
} else if (now >= state.tokenRateFadeStartsAt + TOKEN_RATE_FADE_MS) {
|
|
823
|
-
state.tokenRateText = "";
|
|
824
|
-
}
|
|
825
|
-
}
|
|
801
|
+
const tokenRate = state.tokenRate.display(now);
|
|
826
802
|
const showSpinner = this.spinnerInMessage();
|
|
827
803
|
const msg = isFullyDefaultAppearance(features, decorations) && !showSpinner && !responseModelShown
|
|
828
804
|
? undefined
|
|
@@ -837,15 +813,11 @@ export class SessionManager {
|
|
|
837
813
|
spinnerAtMs: showSpinner ? now : undefined,
|
|
838
814
|
word: state.workingText.text,
|
|
839
815
|
shimmerAtMs: now - state.shimmerOrigin,
|
|
840
|
-
renderMeter: (colorizer, dimmed) => state.activityMeter.
|
|
841
|
-
ActivityMeter.colorizeCell(level, char, ctx.ui.theme, colorizer, dimmed),
|
|
842
|
-
),
|
|
816
|
+
renderMeter: (colorizer, dimmed) => renderMeter(state.activityMeter, ctx.ui.theme, colorizer, dimmed),
|
|
843
817
|
elapsedMs: now - state.startTime,
|
|
844
818
|
outputTokens: total,
|
|
845
|
-
tokenRateText:
|
|
846
|
-
tokenRateShade:
|
|
847
|
-
? undefined
|
|
848
|
-
: Math.floor((now - state.tokenRateFadeStartsAt) / (TOKEN_RATE_FADE_MS / TOKEN_RATE_FADE_SHADE_COUNT)),
|
|
819
|
+
tokenRateText: features.tokenRate && tokenRate.rate !== null ? formatTokenRate(tokenRate.rate) : "",
|
|
820
|
+
tokenRateShade: tokenRate.phase === "fading" ? tokenRate.fadeShade : undefined,
|
|
849
821
|
responseModel: responseModelShown,
|
|
850
822
|
},
|
|
851
823
|
);
|
|
@@ -866,19 +838,17 @@ export class SessionManager {
|
|
|
866
838
|
const before = this.indicatorFingerprint();
|
|
867
839
|
const styleBefore = this.#settings.outputStyle;
|
|
868
840
|
const preview = new PreviewRenderer(ctx, this.#allPacks);
|
|
869
|
-
const result = await showMenu
|
|
841
|
+
const result = await showMenu(ctx, {
|
|
870
842
|
title: "Pi Topping: Settings",
|
|
871
|
-
maxHeight: "75%",
|
|
843
|
+
layout: { maxHeight: "75%" },
|
|
872
844
|
sections: buildMenuSections(this.#settings, this.#bundledPacks, this.#userPacks),
|
|
873
845
|
hints: ["↑↓ move", "PgUp/PgDn page", "←→ select", "␣ toggle", "⏎ apply", "esc cancel"],
|
|
874
|
-
preview
|
|
846
|
+
preview,
|
|
875
847
|
});
|
|
876
848
|
if (!result.applied) return;
|
|
877
849
|
|
|
878
850
|
const updatedSettings = applyMenuResult(this.#settings, result.values);
|
|
879
|
-
|
|
880
|
-
saveSettings(updatedSettings);
|
|
881
|
-
} catch {
|
|
851
|
+
if (!saveSettings(updatedSettings).ok) {
|
|
882
852
|
ctx.ui.notify("Failed to save Pi Topping settings", "error");
|
|
883
853
|
return;
|
|
884
854
|
}
|