@underactive/pi-topping 0.11.1 → 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 +8 -11
- 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 -35
- package/src/pi-installer.ts +39 -56
- package/src/preview.ts +29 -12
- package/src/prompt-decorator.ts +40 -25
- package/src/session.ts +135 -141
- package/src/settings.ts +89 -83
- package/src/setup-check.ts +1 -1
- package/src/setup-command.ts +9 -4
- package/src/util.ts +29 -7
- 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/session.ts
CHANGED
|
@@ -14,16 +14,27 @@ import type {
|
|
|
14
14
|
SessionStartEvent,
|
|
15
15
|
Theme,
|
|
16
16
|
} from "@earendil-works/pi-coding-agent";
|
|
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";
|
|
17
19
|
import { Text, type Component } from "@earendil-works/pi-tui";
|
|
18
|
-
import {
|
|
19
|
-
|
|
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";
|
|
20
29
|
import {
|
|
21
30
|
dimAttribute,
|
|
22
|
-
|
|
31
|
+
fadeThemeColorString,
|
|
32
|
+
renderMeter,
|
|
33
|
+
thinkingLevelColorizer,
|
|
34
|
+
} from "@underactive/pi-topping-primitives/activity-tui";
|
|
35
|
+
import {
|
|
23
36
|
isThinkingLevel,
|
|
24
37
|
ELAPSED_INTERVAL_MS,
|
|
25
|
-
fadeThemeColorString,
|
|
26
|
-
formatElapsed,
|
|
27
38
|
formatTokenRate,
|
|
28
39
|
formatTokens,
|
|
29
40
|
isFullyDefaultAppearance,
|
|
@@ -31,26 +42,30 @@ import {
|
|
|
31
42
|
RESPONSE_MODEL_FADE_MS,
|
|
32
43
|
RESPONSE_MODEL_HOLD_MS,
|
|
33
44
|
RESPONSE_MODEL_SLIDE_FRAME_MS,
|
|
34
|
-
RESPONSE_MODEL_SLIDE_MS,
|
|
35
45
|
SHIMMER_INTERVAL_MS,
|
|
36
|
-
slideOutTail,
|
|
37
46
|
SPINNER_FRAME_MS,
|
|
38
47
|
SPINNER_FRAMES,
|
|
39
|
-
TOKEN_RATE_FADE_SHADE_COUNT,
|
|
40
|
-
StreamingWordCounter,
|
|
41
48
|
type ThinkingLevel,
|
|
42
49
|
} from "./format.ts";
|
|
43
50
|
import { buildLoaderMessage } from "./loader-message.ts";
|
|
44
|
-
import { showMenu } from "
|
|
45
|
-
import { applyOutputStyle,
|
|
51
|
+
import { showMenu } from "@underactive/pi-topping-primitives/menu";
|
|
52
|
+
import { applyOutputStyle, OUTPUT_STYLES } from "./output-style.ts";
|
|
46
53
|
import { getResponseModelColorizer } from "./nvidia-green.ts";
|
|
47
54
|
import { registerSetupCommand } from "./setup-command.ts";
|
|
48
55
|
import { applyMenuResult, buildMenuSections, loadSettings, saveSettings, type ThinkingLevelColor } from "./settings.ts";
|
|
49
|
-
import { announcesEmbedded, STATUSLINE_EMBED_CHANNEL } from "./statusline-embed.ts";
|
|
50
56
|
import { announceMissingToppingsOnce, type MissingToppingsEntryData, renderMissingToppingsEntry, SETUP_ENTRY_TYPE } from "./toppings.ts";
|
|
51
57
|
import { PreviewRenderer } from "./preview.ts";
|
|
52
|
-
import {
|
|
53
|
-
|
|
58
|
+
import {
|
|
59
|
+
buildCompletionMarkerContent,
|
|
60
|
+
buildCompletionMarkerLine,
|
|
61
|
+
PROMPT_BOX_ENTRY_TYPE,
|
|
62
|
+
PROMPT_BOX_TYPE,
|
|
63
|
+
promptBoxEntryRenderer,
|
|
64
|
+
promptBoxRenderer,
|
|
65
|
+
type PromptBoxDetails,
|
|
66
|
+
type PromptBoxEntryData,
|
|
67
|
+
} from "./prompt-decorator.ts";
|
|
68
|
+
import { hostBelow, isPlainObject, modelsResemble, modelsSpelledAlike, sanitizedText, stripControlChars } from "./util.ts";
|
|
54
69
|
import { formatServiceTierSummary, recordServiceTierCall, serviceTierCountsOf, serviceTierOf, type ServiceTierCount } from "./service-tier.ts";
|
|
55
70
|
import { servedModelOf } from "./served-model.ts";
|
|
56
71
|
import { loadBundledWordPacks, loadUserWordPacks, pickWorkingTextSelection, type WorkingTextSelection, type WordPack } from "./word-packs.ts";
|
|
@@ -61,14 +76,16 @@ type MessageEndEvent = Extract<ExtensionEvent, { type: "message_end" }>;
|
|
|
61
76
|
type ToolExecutionStartEvent = Extract<ExtensionEvent, { type: "tool_execution_start" }>;
|
|
62
77
|
type UIPromptStartEvent = Extract<ExtensionEvent, { type: "ui_prompt_start" }>;
|
|
63
78
|
type UIPromptEndEvent = Extract<ExtensionEvent, { type: "ui_prompt_end" }>;
|
|
64
|
-
type ContextWithSystemEvent = Extract<ExtensionEvent, { type: "context_with_system" }>;
|
|
65
79
|
type ProviderStreamEvent = Extract<ExtensionEvent, { type: "provider_stream_event" }>;
|
|
66
80
|
type UIPromptKind = UIPromptStartEvent["kind"];
|
|
67
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Oldest Pi this extension supports. Pi 1.2.0 runs before_agent_start for the run that a custom
|
|
84
|
+
* message starts, which is how the output style reaches a decorated prompt.
|
|
85
|
+
*/
|
|
86
|
+
export const MIN_HOST_VERSION = "1.2.0";
|
|
68
87
|
const DONE_ENTRY_TYPE = "pi-topping-done";
|
|
69
88
|
const RESPONSE_MODEL_STATUS_KEY = "pi-topping-response-model";
|
|
70
|
-
/** Pi's `VIRTUAL_MODEL_API` for extension-registered virtual models; Pi does not export it from its public index. */
|
|
71
|
-
const VIRTUAL_MODEL_API = "pi-virtual";
|
|
72
89
|
const WAITING_LABELS: Record<UIPromptKind, string> = {
|
|
73
90
|
select: "Waiting for selection",
|
|
74
91
|
confirm: "Waiting for confirmation",
|
|
@@ -115,9 +132,7 @@ interface SessionState {
|
|
|
115
132
|
liveTokens: number;
|
|
116
133
|
shimmerOrigin: number;
|
|
117
134
|
activityMeter: ActivityMeter;
|
|
118
|
-
|
|
119
|
-
lastTokenRateSampledAt: number;
|
|
120
|
-
lastTokenRateTotal: number;
|
|
135
|
+
tokenRate: RateHoldFade;
|
|
121
136
|
responseModel: string;
|
|
122
137
|
served: { model: string; backend: string; routedId: string } | null;
|
|
123
138
|
/** The backend companion to the last raw response-model report. */
|
|
@@ -133,8 +148,6 @@ interface SessionState {
|
|
|
133
148
|
/** When the response model appeared in the loader and began sliding out; 0 if it never slid. */
|
|
134
149
|
responseModelShownAt: number;
|
|
135
150
|
responseModelSlideTimer: ReturnType<typeof setInterval> | null;
|
|
136
|
-
tokenRateText: string;
|
|
137
|
-
tokenRateFadeStartsAt: number;
|
|
138
151
|
timer: ReturnType<typeof setInterval> | null;
|
|
139
152
|
busy: boolean;
|
|
140
153
|
waiting: { kind: UIPromptKind; title?: string } | null;
|
|
@@ -146,15 +159,6 @@ interface SessionState {
|
|
|
146
159
|
/** Sentinel distinguishing "never called setWorkingMessage()" from an explicit `undefined` message. */
|
|
147
160
|
const NOT_SENT = Symbol("not-sent");
|
|
148
161
|
|
|
149
|
-
/** Reset all token-rate tracking fields on `state` to their fresh-turn values. */
|
|
150
|
-
function resetTokenRateState(state: SessionState): void {
|
|
151
|
-
state.rateTracker.reset();
|
|
152
|
-
state.lastTokenRateSampledAt = 0;
|
|
153
|
-
state.lastTokenRateTotal = 0;
|
|
154
|
-
state.tokenRateText = "";
|
|
155
|
-
state.tokenRateFadeStartsAt = 0;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
162
|
function makeFreshState(): SessionState {
|
|
159
163
|
return {
|
|
160
164
|
startTime: 0,
|
|
@@ -163,9 +167,12 @@ function makeFreshState(): SessionState {
|
|
|
163
167
|
liveTokens: 0,
|
|
164
168
|
shimmerOrigin: 0,
|
|
165
169
|
activityMeter: new ActivityMeter(),
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}),
|
|
169
176
|
responseModel: "",
|
|
170
177
|
served: null,
|
|
171
178
|
lastResponseModelBackend: NOT_SENT,
|
|
@@ -177,8 +184,6 @@ function makeFreshState(): SessionState {
|
|
|
177
184
|
responseModelFadeGeneration: 0,
|
|
178
185
|
responseModelShownAt: 0,
|
|
179
186
|
responseModelSlideTimer: null,
|
|
180
|
-
tokenRateText: "",
|
|
181
|
-
tokenRateFadeStartsAt: 0,
|
|
182
187
|
timer: null,
|
|
183
188
|
busy: false,
|
|
184
189
|
waiting: null,
|
|
@@ -203,10 +208,14 @@ export class SessionManager {
|
|
|
203
208
|
#currentCtx: ExtensionContext | null = null;
|
|
204
209
|
/** Whether pi-topping-statusline last announced that its status bar hosts the loader; kept across sessions. */
|
|
205
210
|
#loaderInStatusline = false;
|
|
211
|
+
/** Whether this instance showed the old-host warning. */
|
|
212
|
+
#warnedOldHost = false;
|
|
206
213
|
readonly #pi: ExtensionAPI;
|
|
214
|
+
readonly #hostVersion: string;
|
|
207
215
|
|
|
208
|
-
constructor(pi: ExtensionAPI) {
|
|
216
|
+
constructor(pi: ExtensionAPI, hostVersion: string = VERSION) {
|
|
209
217
|
this.#pi = pi;
|
|
218
|
+
this.#hostVersion = hostVersion;
|
|
210
219
|
}
|
|
211
220
|
|
|
212
221
|
#onSessionStart = async (_e: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
@@ -223,37 +232,20 @@ export class SessionManager {
|
|
|
223
232
|
if (this.usable(ctx)) {
|
|
224
233
|
this.applyIndicator(ctx);
|
|
225
234
|
announceMissingToppingsOnce(this.#pi);
|
|
235
|
+
this.warnOldHostOnce(ctx);
|
|
226
236
|
}
|
|
227
237
|
};
|
|
228
238
|
|
|
229
|
-
/** Unlike the UI handlers, this runs in every mode: the output style shapes the model's prompt. */
|
|
230
|
-
#onBeforeAgentStart = (event: BeforeAgentStartEvent): BeforeAgentStartEventResult | undefined =>
|
|
231
|
-
applyOutputStyle(event.systemPromptOptions?.sections, this.#settings.outputStyle);
|
|
232
|
-
|
|
233
239
|
/**
|
|
234
|
-
*
|
|
235
|
-
*
|
|
240
|
+
* Unlike the UI handlers, this runs in every mode: the output style shapes the model's prompt.
|
|
241
|
+
* The prompt section applies to every run. The hidden reminder belongs to a user prompt: one Pi
|
|
242
|
+
* submitted (`messageType: "user"`), or one that pi-topping re-sent as its prompt box, which
|
|
243
|
+
* starts the run as a custom message. A run that another extension's custom message starts gets
|
|
244
|
+
* the section only.
|
|
236
245
|
*/
|
|
237
|
-
#
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
const messages = event.messages.slice();
|
|
241
|
-
let index = messages.length;
|
|
242
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
243
|
-
const message = messages[i];
|
|
244
|
-
if (!message) continue;
|
|
245
|
-
if (message.role === "user" || (message.role === "custom" && message.customType === PROMPT_BOX_TYPE)) {
|
|
246
|
-
index = i;
|
|
247
|
-
break;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
messages.splice(index, 0, {
|
|
251
|
-
role: "system",
|
|
252
|
-
content: "",
|
|
253
|
-
sections: { [OUTPUT_STYLE_SECTION]: patch },
|
|
254
|
-
timestamp: Date.now(),
|
|
255
|
-
});
|
|
256
|
-
return { messages };
|
|
246
|
+
#onBeforeAgentStart = (event: BeforeAgentStartEvent): BeforeAgentStartEventResult | undefined => {
|
|
247
|
+
const result = applyOutputStyle(event.systemPromptOptions?.sections, this.#settings.outputStyle);
|
|
248
|
+
return event.messageType !== "custom" || event.message.customType === PROMPT_BOX_TYPE ? result : undefined;
|
|
257
249
|
};
|
|
258
250
|
|
|
259
251
|
#onInput = async (event: InputEvent, ctx: ExtensionContext): Promise<InputEventResult | void> => {
|
|
@@ -262,40 +254,24 @@ export class SessionManager {
|
|
|
262
254
|
|
|
263
255
|
if (!event.streamingBehavior && !this.#state.busy) this.resetTurn(Date.now());
|
|
264
256
|
else if (event.streamingBehavior && event.source !== "extension") this.#state.midTurnInputs++;
|
|
257
|
+
if (this.#settings.decorations.decorateUserPrompt && this.shouldDecorateSkill(event)) {
|
|
258
|
+
this.#pi.appendEntry<PromptBoxEntryData>(PROMPT_BOX_ENTRY_TYPE, {
|
|
259
|
+
content: event.text,
|
|
260
|
+
details: this.promptBoxDetails(ctx),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
265
263
|
if (this.#settings.decorations.decorateUserPrompt && this.shouldDecorate(event)) {
|
|
266
264
|
try {
|
|
267
|
-
|
|
268
|
-
|
|
265
|
+
// The prompt box starts the run; before_agent_start adds the output style's section and reminder to it.
|
|
266
|
+
this.#pi.sendMessage<PromptBoxDetails>(
|
|
269
267
|
{
|
|
270
268
|
customType: PROMPT_BOX_TYPE,
|
|
271
269
|
content: event.text,
|
|
272
270
|
display: true,
|
|
273
|
-
details:
|
|
274
|
-
submittedAt: Date.now(),
|
|
275
|
-
showIcon: this.#settings.decorations.promptIcon,
|
|
276
|
-
showTimestamp: this.#settings.decorations.promptTimestamp,
|
|
277
|
-
showProvider: this.#settings.decorations.promptProvider,
|
|
278
|
-
showModel: this.#settings.decorations.promptModel,
|
|
279
|
-
icon: this.#settings.decorations.useNerdFont ? "" : "π",
|
|
280
|
-
provider: ctx.model?.provider,
|
|
281
|
-
model: ctx.model?.id,
|
|
282
|
-
borderColor: this.#settings.decorations.borderColor,
|
|
283
|
-
borderStyle: this.#settings.decorations.borderStyle,
|
|
284
|
-
thinkingLevel: ctx.thinkingLevel,
|
|
285
|
-
},
|
|
271
|
+
details: this.promptBoxDetails(ctx),
|
|
286
272
|
},
|
|
287
|
-
{ triggerTurn:
|
|
273
|
+
{ triggerTurn: true },
|
|
288
274
|
);
|
|
289
|
-
if (stylePrompt) {
|
|
290
|
-
await this.#pi.sendMessage(
|
|
291
|
-
{
|
|
292
|
-
customType: OUTPUT_STYLE_MESSAGE_TYPE,
|
|
293
|
-
content: stylePrompt.reminder,
|
|
294
|
-
display: false,
|
|
295
|
-
},
|
|
296
|
-
{ triggerTurn: true },
|
|
297
|
-
);
|
|
298
|
-
}
|
|
299
275
|
return { action: "handled" };
|
|
300
276
|
} catch {
|
|
301
277
|
ctx.ui.notify("Failed to submit prompt", "error");
|
|
@@ -349,11 +325,9 @@ export class SessionManager {
|
|
|
349
325
|
typeof rawTokens === "number" && Number.isFinite(rawTokens) && rawTokens >= 0 ? rawTokens : undefined;
|
|
350
326
|
this.#state.confirmTokens += exactTokens ?? this.#state.liveTokens;
|
|
351
327
|
if (exactTokens !== undefined) {
|
|
352
|
-
// Only
|
|
328
|
+
// Only restart the sampling here, not the held rate or its fade:
|
|
353
329
|
// those must keep holding/fading across message boundaries to avoid flicker.
|
|
354
|
-
this.#state.
|
|
355
|
-
this.#state.lastTokenRateSampledAt = 0;
|
|
356
|
-
this.#state.lastTokenRateTotal = this.#state.confirmTokens;
|
|
330
|
+
this.#state.tokenRate.resetSampling(this.#state.confirmTokens);
|
|
357
331
|
}
|
|
358
332
|
this.#state.liveTokens = 0;
|
|
359
333
|
this.#counter.reset();
|
|
@@ -422,7 +396,7 @@ export class SessionManager {
|
|
|
422
396
|
ctx.ui.setWorkingMessage();
|
|
423
397
|
if (responseModel) this.startResponseModelFade(ctx, responseModel, responseModelColor, responseModelDimmed);
|
|
424
398
|
this.#state.activityMeter.reset();
|
|
425
|
-
|
|
399
|
+
this.#state.tokenRate.reset();
|
|
426
400
|
this.#state.lastMessage = NOT_SENT;
|
|
427
401
|
if (this.#settings.features.doneMarker && hadPrompt) {
|
|
428
402
|
const aborted = e.aborted === true;
|
|
@@ -466,10 +440,10 @@ export class SessionManager {
|
|
|
466
440
|
const thinkingLevel = isThinkingLevel(entry.data.thinkingLevel) ? entry.data.thinkingLevel : undefined;
|
|
467
441
|
const rawModel = sanitizedText(entry.data.model);
|
|
468
442
|
const modelColored = features.doneMarkerModel && rawModel
|
|
469
|
-
?
|
|
443
|
+
? thinkingLevelColorizer(theme, this.#settings.decorations.doneMarkerModelColor, thinkingLevel)(rawModel)
|
|
470
444
|
: "";
|
|
471
445
|
const model = modelColored && this.#settings.decorations.doneMarkerModelDimmed ? dimAttribute(modelColored) : modelColored;
|
|
472
|
-
const markerContent = buildCompletionMarkerContent(theme, icon, word,
|
|
446
|
+
const markerContent = buildCompletionMarkerContent(theme, icon, word, formatElapsedUnits(elapsedMs), details, model, aborted ? "warning" : "dim");
|
|
473
447
|
const borderStyle = this.#settings.decorations.doneMarkerBorderStyle;
|
|
474
448
|
const borderColor = aborted ? "warning" : this.#settings.decorations.doneMarkerBorderColor;
|
|
475
449
|
const markerStyle = this.#settings.decorations.doneMarkerStyle;
|
|
@@ -495,7 +469,6 @@ export class SessionManager {
|
|
|
495
469
|
install(): void {
|
|
496
470
|
this.#pi.on("session_start", this.#onSessionStart);
|
|
497
471
|
this.#pi.on("before_agent_start", this.#onBeforeAgentStart);
|
|
498
|
-
this.#pi.on("context_with_system", this.#onContextWithSystem);
|
|
499
472
|
this.#pi.on("input", this.#onInput);
|
|
500
473
|
this.#pi.on("agent_start", this.#onAgentStart);
|
|
501
474
|
this.#pi.on("message_start", this.#onMessageStart);
|
|
@@ -507,18 +480,18 @@ export class SessionManager {
|
|
|
507
480
|
this.#pi.on("ui_prompt_start", this.#onUIPromptStart);
|
|
508
481
|
this.#pi.on("ui_prompt_end", this.#onUIPromptEnd);
|
|
509
482
|
this.#pi.on("session_shutdown", this.#onSessionShutdown);
|
|
510
|
-
this.#pi.events.
|
|
511
|
-
this.#loaderInStatusline =
|
|
483
|
+
subscribe(this.#pi.events, CHANNELS.workingStatusEmbedded, ({ embedded }) => {
|
|
484
|
+
this.#loaderInStatusline = embedded;
|
|
512
485
|
});
|
|
513
|
-
this.#pi.events.
|
|
486
|
+
subscribe(this.#pi.events, CHANNELS.routedModel, (announcement) => {
|
|
514
487
|
// Direct routes serve compaction and other extensions' side requests; a route while idle
|
|
515
488
|
// is such a background request too, and must not leave a stale model for the next turn.
|
|
516
|
-
|
|
517
|
-
if (!announcement || announcement.reason === "direct" || !this.#state.busy) return;
|
|
489
|
+
if (announcement.reason === "direct" || !this.#state.busy) return;
|
|
518
490
|
this.updateResponseModel({ raw: announcement.modelId, weak: true, provider: announcement.provider, routedId: announcement.modelId });
|
|
519
491
|
});
|
|
520
492
|
this.#pi.registerEntryRenderer<DoneEntryData>(DONE_ENTRY_TYPE, (entry, _o, theme) => this.#renderDoneEntry(entry, theme));
|
|
521
493
|
this.#pi.registerEntryRenderer<MissingToppingsEntryData>(SETUP_ENTRY_TYPE, (entry, _o, theme) => renderMissingToppingsEntry(entry, theme));
|
|
494
|
+
this.#pi.registerEntryRenderer<PromptBoxEntryData>(PROMPT_BOX_ENTRY_TYPE, promptBoxEntryRenderer);
|
|
522
495
|
this.#pi.registerMessageRenderer<PromptBoxDetails>(PROMPT_BOX_TYPE, promptBoxRenderer);
|
|
523
496
|
this.#pi.registerCommand("topping-settings", {
|
|
524
497
|
description: "Configure the prompt box, working loader, completion marker, word packs, and output style.",
|
|
@@ -531,11 +504,49 @@ export class SessionManager {
|
|
|
531
504
|
return !!ctx.hasUI && ctx.mode !== "print";
|
|
532
505
|
}
|
|
533
506
|
|
|
507
|
+
private warnOldHostOnce(ctx: ExtensionContext): void {
|
|
508
|
+
if (this.#warnedOldHost || !hostBelow(this.#hostVersion, MIN_HOST_VERSION)) return;
|
|
509
|
+
this.#warnedOldHost = true;
|
|
510
|
+
ctx.ui.notify(
|
|
511
|
+
`Pi Topping needs Pi ${MIN_HOST_VERSION} or later; this is Pi ${this.#hostVersion}. Output styles will not apply to decorated prompts.`,
|
|
512
|
+
"warning",
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
|
|
534
516
|
/**
|
|
535
517
|
* Mid-stream inputs (steer/follow-up) must pass through untouched: re-sending them via
|
|
536
518
|
* sendMessage(deliverAs) bypasses Pi's queue state, so the "Steering:"/"Follow-up:" rows
|
|
537
519
|
* below the editor never render (issue #2).
|
|
538
520
|
*/
|
|
521
|
+
private promptBoxDetails(ctx: ExtensionContext): PromptBoxDetails {
|
|
522
|
+
return {
|
|
523
|
+
submittedAt: Date.now(),
|
|
524
|
+
showIcon: this.#settings.decorations.promptIcon,
|
|
525
|
+
showTimestamp: this.#settings.decorations.promptTimestamp,
|
|
526
|
+
showProvider: this.#settings.decorations.promptProvider,
|
|
527
|
+
showModel: this.#settings.decorations.promptModel,
|
|
528
|
+
icon: this.#settings.decorations.useNerdFont ? "" : "π",
|
|
529
|
+
provider: ctx.model?.provider,
|
|
530
|
+
model: ctx.model?.id,
|
|
531
|
+
borderColor: this.#settings.decorations.borderColor,
|
|
532
|
+
borderStyle: this.#settings.decorations.borderStyle,
|
|
533
|
+
thinkingLevel: ctx.thinkingLevel,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private shouldDecorateSkill(event: InputEvent): boolean {
|
|
538
|
+
if (
|
|
539
|
+
event.streamingBehavior ||
|
|
540
|
+
event.source === "extension" ||
|
|
541
|
+
!event.text.trim() ||
|
|
542
|
+
event.images?.length ||
|
|
543
|
+
!event.text.startsWith("/")
|
|
544
|
+
) return false;
|
|
545
|
+
|
|
546
|
+
const commandName = event.text.slice(1).split(" ", 1)[0]!;
|
|
547
|
+
return this.#pi.getCommands().some((command) => command.source === "skill" && command.name === commandName);
|
|
548
|
+
}
|
|
549
|
+
|
|
539
550
|
private shouldDecorate(event: InputEvent): boolean {
|
|
540
551
|
return (
|
|
541
552
|
!event.streamingBehavior &&
|
|
@@ -574,7 +585,7 @@ export class SessionManager {
|
|
|
574
585
|
|
|
575
586
|
const color = this.#settings.decorations.spinnerColor;
|
|
576
587
|
ctx.ui.setWorkingIndicator({
|
|
577
|
-
frames: SPINNER_FRAMES.map((frame) =>
|
|
588
|
+
frames: SPINNER_FRAMES.map((frame) => thinkingLevelColorizer(ctx.ui.theme, color, ctx.thinkingLevel)(frame)),
|
|
578
589
|
});
|
|
579
590
|
}
|
|
580
591
|
|
|
@@ -595,12 +606,13 @@ export class SessionManager {
|
|
|
595
606
|
if (message.responseModel !== undefined) {
|
|
596
607
|
return { raw: message.responseModel, weak: false, routedId: sanitizedText(message.model) || undefined };
|
|
597
608
|
}
|
|
598
|
-
if (this.#currentCtx?.model?.api !==
|
|
609
|
+
if (this.#currentCtx?.model?.api !== PI_VIRTUAL_MODEL_API) return undefined;
|
|
599
610
|
return { raw: message.model, weak: true, provider: typeof message.provider === "string" ? message.provider : undefined };
|
|
600
611
|
}
|
|
601
612
|
|
|
602
613
|
/**
|
|
603
|
-
* Whether a response model merely restates the selected model
|
|
614
|
+
* Whether a response model merely restates the selected model: a decorated form of its id,
|
|
615
|
+
* or its id or display name written with other case and separators. Under a virtual selection
|
|
604
616
|
* the routed model is always another model, so only a failed route, which leaves the
|
|
605
617
|
* virtual model itself on the message, is hidden; a resemblance check would also hide a
|
|
606
618
|
* routed model whose id contains the virtual id, such as openrouter's `auto`.
|
|
@@ -608,8 +620,8 @@ export class SessionManager {
|
|
|
608
620
|
private restatesSelection(report: ResponseModelReport, responseModel: string): boolean {
|
|
609
621
|
const selected = this.#currentCtx?.model;
|
|
610
622
|
if (!selected) return false;
|
|
611
|
-
if (selected.api ===
|
|
612
|
-
return modelsResemble(selected.id, responseModel);
|
|
623
|
+
if (selected.api === PI_VIRTUAL_MODEL_API) return report.provider === selected.provider && responseModel === selected.id;
|
|
624
|
+
return modelsResemble(selected.id, responseModel) || modelsSpelledAlike(selected, responseModel);
|
|
613
625
|
}
|
|
614
626
|
|
|
615
627
|
private updateResponseModel(report: ResponseModelReport | undefined): void {
|
|
@@ -660,7 +672,7 @@ export class SessionManager {
|
|
|
660
672
|
state.responseModelShownAt = Date.now();
|
|
661
673
|
state.responseModelSlideTimer = setInterval(() => {
|
|
662
674
|
this.tick();
|
|
663
|
-
if (Date.now() - state.responseModelShownAt >=
|
|
675
|
+
if (Date.now() - state.responseModelShownAt >= WORKING_STATUS_SLIDE_MS) this.stopResponseModelSlide();
|
|
664
676
|
}, RESPONSE_MODEL_SLIDE_FRAME_MS);
|
|
665
677
|
}
|
|
666
678
|
|
|
@@ -713,12 +725,12 @@ export class SessionManager {
|
|
|
713
725
|
render(shade++);
|
|
714
726
|
this.#state.responseModelFadeTimer = setInterval(() => {
|
|
715
727
|
if (this.#state.responseModelFadeGeneration !== generation) return;
|
|
716
|
-
if (shade >=
|
|
728
|
+
if (shade >= FADE_SHADE_COUNT) {
|
|
717
729
|
this.cancelResponseModelFade(ctx);
|
|
718
730
|
return;
|
|
719
731
|
}
|
|
720
732
|
render(shade++);
|
|
721
|
-
}, RESPONSE_MODEL_FADE_MS /
|
|
733
|
+
}, RESPONSE_MODEL_FADE_MS / FADE_SHADE_COUNT);
|
|
722
734
|
}, RESPONSE_MODEL_HOLD_MS);
|
|
723
735
|
}
|
|
724
736
|
|
|
@@ -755,7 +767,7 @@ export class SessionManager {
|
|
|
755
767
|
state.lastResponseModelRaw = NOT_SENT;
|
|
756
768
|
state.lastResponseModelBackend = NOT_SENT;
|
|
757
769
|
state.activityMeter.reset();
|
|
758
|
-
|
|
770
|
+
state.tokenRate.reset();
|
|
759
771
|
state.midTurnInputs = 0;
|
|
760
772
|
state.lastMessage = NOT_SENT;
|
|
761
773
|
this.#counter.reset();
|
|
@@ -778,27 +790,15 @@ export class SessionManager {
|
|
|
778
790
|
const total = state.confirmTokens + state.liveTokens;
|
|
779
791
|
const features = this.#settings.features;
|
|
780
792
|
const decorations = this.#settings.decorations;
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
state.lastTokenRateTotal = total;
|
|
786
|
-
const tokenRate = state.rateTracker.sample(total, now);
|
|
787
|
-
if (decorations.tokenActivityMonitor) state.activityMeter.push(rateToLevel(tokenRate, decorations.meterUpperBoundTps));
|
|
788
|
-
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));
|
|
789
797
|
}
|
|
790
798
|
const responseModelShown = features.responseModel && state.responseModel
|
|
791
799
|
? slideOutTail(state.responseModel, now - state.responseModelShownAt)
|
|
792
800
|
: "";
|
|
793
|
-
|
|
794
|
-
const latestTokenRate = formatTokenRate(state.rateTracker.tokenRate);
|
|
795
|
-
if (latestTokenRate && hasNewTokenCount) {
|
|
796
|
-
state.tokenRateText = latestTokenRate;
|
|
797
|
-
state.tokenRateFadeStartsAt = now + TOKEN_RATE_HOLD_MS;
|
|
798
|
-
} else if (now >= state.tokenRateFadeStartsAt + TOKEN_RATE_FADE_MS) {
|
|
799
|
-
state.tokenRateText = "";
|
|
800
|
-
}
|
|
801
|
-
}
|
|
801
|
+
const tokenRate = state.tokenRate.display(now);
|
|
802
802
|
const showSpinner = this.spinnerInMessage();
|
|
803
803
|
const msg = isFullyDefaultAppearance(features, decorations) && !showSpinner && !responseModelShown
|
|
804
804
|
? undefined
|
|
@@ -813,15 +813,11 @@ export class SessionManager {
|
|
|
813
813
|
spinnerAtMs: showSpinner ? now : undefined,
|
|
814
814
|
word: state.workingText.text,
|
|
815
815
|
shimmerAtMs: now - state.shimmerOrigin,
|
|
816
|
-
renderMeter: (colorizer, dimmed) => state.activityMeter.
|
|
817
|
-
ActivityMeter.colorizeCell(level, char, ctx.ui.theme, colorizer, dimmed),
|
|
818
|
-
),
|
|
816
|
+
renderMeter: (colorizer, dimmed) => renderMeter(state.activityMeter, ctx.ui.theme, colorizer, dimmed),
|
|
819
817
|
elapsedMs: now - state.startTime,
|
|
820
818
|
outputTokens: total,
|
|
821
|
-
tokenRateText:
|
|
822
|
-
tokenRateShade:
|
|
823
|
-
? undefined
|
|
824
|
-
: 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,
|
|
825
821
|
responseModel: responseModelShown,
|
|
826
822
|
},
|
|
827
823
|
);
|
|
@@ -842,19 +838,17 @@ export class SessionManager {
|
|
|
842
838
|
const before = this.indicatorFingerprint();
|
|
843
839
|
const styleBefore = this.#settings.outputStyle;
|
|
844
840
|
const preview = new PreviewRenderer(ctx, this.#allPacks);
|
|
845
|
-
const result = await showMenu
|
|
841
|
+
const result = await showMenu(ctx, {
|
|
846
842
|
title: "Pi Topping: Settings",
|
|
847
|
-
maxHeight: "75%",
|
|
843
|
+
layout: { maxHeight: "75%" },
|
|
848
844
|
sections: buildMenuSections(this.#settings, this.#bundledPacks, this.#userPacks),
|
|
849
845
|
hints: ["↑↓ move", "PgUp/PgDn page", "←→ select", "␣ toggle", "⏎ apply", "esc cancel"],
|
|
850
|
-
preview
|
|
846
|
+
preview,
|
|
851
847
|
});
|
|
852
848
|
if (!result.applied) return;
|
|
853
849
|
|
|
854
850
|
const updatedSettings = applyMenuResult(this.#settings, result.values);
|
|
855
|
-
|
|
856
|
-
saveSettings(updatedSettings);
|
|
857
|
-
} catch {
|
|
851
|
+
if (!saveSettings(updatedSettings).ok) {
|
|
858
852
|
ctx.ui.notify("Failed to save Pi Topping settings", "error");
|
|
859
853
|
return;
|
|
860
854
|
}
|