@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/settings.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { DEFAULT_METER_UPPER_BOUND_TPS, METER_UPPER_BOUND_TPS_MAX, METER_UPPER_BOUND_TPS_MIN } from "
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createSettingsStore, type SettingsWriteResult } from "@underactive/pi-topping-primitives/settings";
|
|
4
|
+
import { DEFAULT_METER_UPPER_BOUND_TPS, METER_UPPER_BOUND_TPS_MAX, METER_UPPER_BOUND_TPS_MIN } from "@underactive/pi-topping-primitives/activity";
|
|
5
5
|
import { DEFAULT_LOADER_ORDER, type LoaderElement } from "./format.ts";
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
6
|
+
import { isOutputStyleId, OUTPUT_STYLE_IDS, OUTPUT_STYLES, type OutputStyleId } from "./output-style.ts";
|
|
7
|
+
import type { MenuItem, MenuSection, MenuValue } from "@underactive/pi-topping-primitives/menu";
|
|
8
8
|
import { isWordPackEnabled, isWordPackId, WORD_PACK_MENU_PREFIX, type WordPack } from "./word-packs.ts";
|
|
9
9
|
import { isPlainObject } from "./util.ts";
|
|
10
10
|
|
|
@@ -31,7 +31,7 @@ export const DONE_MARKER_BORDER_STYLE_VALUES = [...BORDER_STYLE_VALUES, "none"]
|
|
|
31
31
|
export type DoneMarkerBorderStyle = (typeof DONE_MARKER_BORDER_STYLE_VALUES)[number];
|
|
32
32
|
export const DONE_MARKER_STYLE_VALUES = ["elite", "bookend"] as const;
|
|
33
33
|
export type DoneMarkerStyle = (typeof DONE_MARKER_STYLE_VALUES)[number];
|
|
34
|
-
export const METER_UPPER_BOUND_TPS_PRESETS = [
|
|
34
|
+
export const METER_UPPER_BOUND_TPS_PRESETS = [10, 20, 30, 40, 50, 60, 80, 100, 150, 200, 300, 500, 1000] as const;
|
|
35
35
|
|
|
36
36
|
export function isBorderStyle(value: unknown): value is BorderStyle {
|
|
37
37
|
return typeof value === "string" && BORDER_STYLE_VALUES.some(style => style === value);
|
|
@@ -169,16 +169,8 @@ export function parseLoaderOrder(value: unknown): LoaderElement[] {
|
|
|
169
169
|
|
|
170
170
|
export function settingsPath(): string { return join(getAgentDir(), "pi-topping", "settings.json"); }
|
|
171
171
|
|
|
172
|
-
/** pi-topping-insights' saved style, honored until pi-topping's own settings record one. */
|
|
173
|
-
function legacyOutputStyle(): OutputStyleId {
|
|
174
|
-
try {
|
|
175
|
-
const parsed: unknown = JSON.parse(readFileSync(join(getAgentDir(), "pi-topping-insights", "settings.json"), "utf8"));
|
|
176
|
-
return (isPlainObject(parsed) ? fromLegacyOutputStyle(parsed.style) : undefined) ?? DEFAULT_SETTINGS.outputStyle;
|
|
177
|
-
} catch { return DEFAULT_SETTINGS.outputStyle; }
|
|
178
|
-
}
|
|
179
|
-
|
|
180
172
|
function defaultSettings(): DecoratorSettings {
|
|
181
|
-
return
|
|
173
|
+
return structuredClone(DEFAULT_SETTINGS);
|
|
182
174
|
}
|
|
183
175
|
|
|
184
176
|
function mergeGroup<T extends Record<string, boolean | string | number>>(defaults: T, parsed: unknown): T {
|
|
@@ -206,8 +198,9 @@ type DecorationSettings = DecoratorSettings["decorations"];
|
|
|
206
198
|
type FeatureSettings = DecoratorSettings["features"];
|
|
207
199
|
type DecorationBooleanKey = { [Key in keyof DecorationSettings]: DecorationSettings[Key] extends boolean ? Key : never }[keyof DecorationSettings];
|
|
208
200
|
type MenuEntryBase = { id: string; label: string; section: MenuSectionName };
|
|
209
|
-
type
|
|
210
|
-
type
|
|
201
|
+
type NumberMenuEntry = { min: number; max: number; unit?: string; presets: readonly number[] };
|
|
202
|
+
type DecorationMenuEntry = MenuEntryBase & { group: "decorations"; key: keyof DecorationSettings; cycleValues?: readonly string[]; cycleValueLabels?: Readonly<Record<string, string>>; cycleEnabledBy?: DecorationBooleanKey; cycleDisabledValue?: string; number?: NumberMenuEntry };
|
|
203
|
+
type FeatureMenuEntry = MenuEntryBase & { group: "features"; key: keyof FeatureSettings; cycleValues?: never; cycleValueLabels?: never; cycleEnabledBy?: never; cycleDisabledValue?: never; number?: never };
|
|
211
204
|
type MenuEntry = DecorationMenuEntry | FeatureMenuEntry;
|
|
212
205
|
const THINKING_LEVEL_CYCLE_LABELS = { "thinking-level": "thinkingLevel" } as const;
|
|
213
206
|
const DIRECTION_CYCLE_LABELS = { ltr: "Left to Right", rtl: "Right to Left" } as const;
|
|
@@ -231,7 +224,7 @@ export const MENU_ENTRIES: readonly MenuEntry[] = [
|
|
|
231
224
|
{ id: "meterColor", label: "Token activity monitor color", section: "“Working” Loader", group: "decorations", key: "meterColor", cycleValues: THINKING_LEVEL_COLOR_VALUES, cycleValueLabels: THINKING_LEVEL_CYCLE_LABELS, cycleEnabledBy: "meterColorEnabled", cycleDisabledValue: "accent" },
|
|
232
225
|
{ id: "meterDirection", label: "Token activity monitor direction", section: "“Working” Loader", group: "decorations", key: "meterDirection", cycleValues: ["ltr", "rtl"], cycleValueLabels: DIRECTION_CYCLE_LABELS, cycleEnabledBy: "meterDirectionEnabled", cycleDisabledValue: "rtl" },
|
|
233
226
|
{ id: "meterDimmed", label: "Token activity monitor dimmed", section: "“Working” Loader", group: "decorations", key: "meterDimmed" },
|
|
234
|
-
{ id: "meterUpperBoundTps", label: "Token activity monitor upper bound", section: "“Working” Loader", group: "decorations", key: "meterUpperBoundTps",
|
|
227
|
+
{ id: "meterUpperBoundTps", label: "Token activity monitor upper bound", section: "“Working” Loader", group: "decorations", key: "meterUpperBoundTps", number: { min: METER_UPPER_BOUND_TPS_MIN, max: METER_UPPER_BOUND_TPS_MAX, unit: "tps", presets: METER_UPPER_BOUND_TPS_PRESETS } },
|
|
235
228
|
{ id: "elapsedTime", label: "Elapsed time since prompt", section: "“Working” Loader", group: "features", key: "elapsedTime" },
|
|
236
229
|
{ id: "outputTokens", label: "Show output tokens", section: "“Working” Loader", group: "features", key: "outputTokens" },
|
|
237
230
|
// id differs from key: the Elements Order row already owns "tokenRate" in the menu's shared value namespace.
|
|
@@ -259,12 +252,26 @@ export const MENU_ENTRIES: readonly MenuEntry[] = [
|
|
|
259
252
|
{ id: "includeDefaultWorkingText", label: "Include default/random “Working” text", section: "Word Packs", group: "features", key: "includeDefaultWorkingText" },
|
|
260
253
|
];
|
|
261
254
|
|
|
262
|
-
function menuItem(entry: MenuEntry, settings: DecoratorSettings):
|
|
255
|
+
function menuItem(entry: MenuEntry, settings: DecoratorSettings): MenuItem {
|
|
263
256
|
const value = entry.group === "decorations" ? settings.decorations[entry.key] : settings.features[entry.key];
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
257
|
+
if (entry.group === "decorations" && entry.number) {
|
|
258
|
+
return { kind: "number", id: entry.id, label: entry.label, value: typeof value === "number" ? value : DEFAULT_METER_UPPER_BOUND_TPS, ...entry.number };
|
|
259
|
+
}
|
|
260
|
+
if (entry.group === "decorations" && entry.cycleValues) {
|
|
261
|
+
const gate = entry.cycleEnabledBy
|
|
262
|
+
? { id: entry.cycleEnabledBy, enabled: settings.decorations[entry.cycleEnabledBy], ...(entry.cycleDisabledValue === undefined ? {} : { disabledValue: entry.cycleDisabledValue }) }
|
|
263
|
+
: undefined;
|
|
264
|
+
return {
|
|
265
|
+
kind: "cycle",
|
|
266
|
+
id: entry.id,
|
|
267
|
+
label: entry.label,
|
|
268
|
+
value: String(value),
|
|
269
|
+
values: entry.cycleValues,
|
|
270
|
+
...(entry.cycleValueLabels ? { labels: entry.cycleValueLabels } : {}),
|
|
271
|
+
...(gate ? { gate } : {}),
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
return { kind: "toggle", id: entry.id, label: entry.label, value: value === true };
|
|
268
275
|
}
|
|
269
276
|
|
|
270
277
|
function isDecorationBooleanKey(key: keyof DecorationSettings): key is DecorationBooleanKey {
|
|
@@ -317,12 +324,12 @@ function buildSection(title: MenuSectionName, settings: DecoratorSettings): Menu
|
|
|
317
324
|
|
|
318
325
|
export function buildMenuSections(settings: DecoratorSettings, bundledPacks: readonly WordPack[], userPacks: readonly WordPack[] = []): MenuSection[] {
|
|
319
326
|
const packs = [...bundledPacks, ...userPacks];
|
|
320
|
-
const packItems = packs.map((pack) => ({ id: `${WORD_PACK_MENU_PREFIX}${pack.id}`, label: pack.name, value: isWordPackEnabled(pack.id, settings.wordPacks) }));
|
|
327
|
+
const packItems: MenuItem[] = packs.map((pack) => ({ kind: "toggle", id: `${WORD_PACK_MENU_PREFIX}${pack.id}`, label: pack.name, value: isWordPackEnabled(pack.id, settings.wordPacks) }));
|
|
321
328
|
const packIds = packItems.map(item => item.id);
|
|
322
329
|
return [
|
|
323
330
|
buildSection("User Prompt", settings),
|
|
324
331
|
buildSection("“Working” Loader", settings),
|
|
325
|
-
{ title: "Elements Order", items: parseLoaderOrder(settings.loaderOrder).map(id => ({ id, label: LOADER_ELEMENT_LABELS[id],
|
|
332
|
+
{ title: "Elements Order", items: parseLoaderOrder(settings.loaderOrder).map(id => ({ kind: "reorder" as const, id, label: LOADER_ELEMENT_LABELS[id], group: LOADER_ORDER_ID })) },
|
|
326
333
|
buildSection("Completion Marker", settings),
|
|
327
334
|
{
|
|
328
335
|
title: "Word Packs",
|
|
@@ -333,18 +340,21 @@ export function buildMenuSections(settings: DecoratorSettings, bundledPacks: rea
|
|
|
333
340
|
},
|
|
334
341
|
{
|
|
335
342
|
title: "Output Style",
|
|
336
|
-
items: [{ id: OUTPUT_STYLE_ID, label: "Style", value: settings.outputStyle,
|
|
343
|
+
items: [{ kind: "cycle", id: OUTPUT_STYLE_ID, label: "Style", value: settings.outputStyle, values: OUTPUT_STYLE_IDS, labels: Object.fromEntries(OUTPUT_STYLE_IDS.map(id => [id, OUTPUT_STYLES[id].label])) }],
|
|
337
344
|
},
|
|
338
345
|
buildSection("Options", settings),
|
|
339
346
|
];
|
|
340
347
|
}
|
|
341
348
|
|
|
342
|
-
export function applyMenuResult(settings: DecoratorSettings, values: Record<string,
|
|
349
|
+
export function applyMenuResult(settings: DecoratorSettings, values: Record<string, MenuValue>): DecoratorSettings {
|
|
343
350
|
const next = structuredClone(settings);
|
|
344
351
|
for (const entry of MENU_ENTRIES) {
|
|
345
352
|
const value = values[entry.id];
|
|
346
353
|
if (value === undefined) continue;
|
|
347
|
-
if (entry.group === "decorations" && entry.
|
|
354
|
+
if (entry.group === "decorations" && entry.number && (typeof value === "number" || typeof value === "string")) {
|
|
355
|
+
// The shared menu publishes a number; a decimal string from an older caller still parses.
|
|
356
|
+
setDecorationCycleValue(next.decorations, entry.key, String(value));
|
|
357
|
+
} else if (entry.group === "decorations" && entry.cycleValues && typeof value === "string" && entry.cycleValues.includes(value)) {
|
|
348
358
|
setDecorationCycleValue(next.decorations, entry.key, value);
|
|
349
359
|
if (entry.cycleEnabledBy) next.decorations[entry.cycleEnabledBy] = values[entry.cycleEnabledBy] !== false;
|
|
350
360
|
} else if (entry.group === "decorations" && typeof value === "boolean" && isDecorationBooleanKey(entry.key)) {
|
|
@@ -375,67 +385,63 @@ export function applyMenuResult(settings: DecoratorSettings, values: Record<stri
|
|
|
375
385
|
* setting. A missing, non-integer, or out-of-range meter upper bound resolves
|
|
376
386
|
* to the default of 80 tps. A legacy `features.simCityWorkingText: true` enables the
|
|
377
387
|
* `simcity` pack when no explicit preference is stored. A missing or invalid `outputStyle`
|
|
378
|
-
* falls back to `
|
|
388
|
+
* falls back to `default`, without a schema-version gate.
|
|
379
389
|
*/
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
for (const [id, enabled] of Object.entries(parsed.wordPacks)) {
|
|
387
|
-
if (isWordPackId(id) && typeof enabled === "boolean") wordPacks[id] = enabled;
|
|
388
|
-
}
|
|
390
|
+
function sanitizeSettings(parsed: unknown): DecoratorSettings {
|
|
391
|
+
if (!isPlainObject(parsed)) return defaultSettings();
|
|
392
|
+
const wordPacks: Record<string, boolean> = { ...DEFAULT_SETTINGS.wordPacks };
|
|
393
|
+
if (isPlainObject(parsed.wordPacks)) {
|
|
394
|
+
for (const [id, enabled] of Object.entries(parsed.wordPacks)) {
|
|
395
|
+
if (isWordPackId(id) && typeof enabled === "boolean") wordPacks[id] = enabled;
|
|
389
396
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const bound = parseMeterUpperBoundTps(parsed.decorations.meterUpperBoundTps);
|
|
405
|
-
if (bound !== undefined) settings.decorations.meterUpperBoundTps = bound;
|
|
406
|
-
}
|
|
407
|
-
const schemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 1;
|
|
408
|
-
if (schemaVersion < THINKING_LEVEL_COLOR_MIGRATION_VERSION) {
|
|
409
|
-
settings.decorations.spinnerColor = "thinking-level";
|
|
410
|
-
if (settings.decorations.borderColor === "borderAccent") settings.decorations.borderColor = "thinking-level";
|
|
411
|
-
}
|
|
412
|
-
if (schemaVersion < DONE_MARKER_BORDER_COLOR_MIGRATION_VERSION) {
|
|
413
|
-
settings.decorations.doneMarkerBorderColor = "thinking-level";
|
|
414
|
-
}
|
|
415
|
-
if (schemaVersion < RESPONSE_MODEL_FOOTER_MIGRATION_VERSION) {
|
|
416
|
-
settings.features.responseModelFooter = settings.features.responseModel;
|
|
397
|
+
}
|
|
398
|
+
if (isPlainObject(parsed.features) && parsed.features.simCityWorkingText === true && !("simcity" in wordPacks)) {
|
|
399
|
+
wordPacks.simcity = true;
|
|
400
|
+
}
|
|
401
|
+
const storedOutputStyle = parsed.outputStyle;
|
|
402
|
+
const outputStyle = isOutputStyleId(storedOutputStyle) ? storedOutputStyle : DEFAULT_SETTINGS.outputStyle;
|
|
403
|
+
const settings = { decorations: mergeGroup(DEFAULT_SETTINGS.decorations, parsed.decorations), features: mergeGroup(DEFAULT_SETTINGS.features, parsed.features), loaderOrder: parseLoaderOrder(parsed.loaderOrder), wordPacks, outputStyle };
|
|
404
|
+
if (isPlainObject(parsed.decorations)) {
|
|
405
|
+
for (const [key, value] of Object.entries(parsed.decorations)) {
|
|
406
|
+
if (!Object.hasOwn(DEFAULT_SETTINGS.decorations, key) || typeof value !== "string") continue;
|
|
407
|
+
const k = key as keyof DecorationSettings;
|
|
408
|
+
if (isDecorationBooleanKey(k)) continue;
|
|
409
|
+
setDecorationCycleValue(settings.decorations, k,
|
|
410
|
+
value === "default" && (k === "spinnerColor" || k === "doneMarkerBorderColor") ? "thinking-level" : value);
|
|
417
411
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
412
|
+
const bound = parseMeterUpperBoundTps(parsed.decorations.meterUpperBoundTps);
|
|
413
|
+
if (bound !== undefined) settings.decorations.meterUpperBoundTps = bound;
|
|
414
|
+
}
|
|
415
|
+
const schemaVersion = typeof parsed.schemaVersion === "number" ? parsed.schemaVersion : 1;
|
|
416
|
+
if (schemaVersion < THINKING_LEVEL_COLOR_MIGRATION_VERSION) {
|
|
417
|
+
settings.decorations.spinnerColor = "thinking-level";
|
|
418
|
+
if (settings.decorations.borderColor === "borderAccent") settings.decorations.borderColor = "thinking-level";
|
|
419
|
+
}
|
|
420
|
+
if (schemaVersion < DONE_MARKER_BORDER_COLOR_MIGRATION_VERSION) {
|
|
421
|
+
settings.decorations.doneMarkerBorderColor = "thinking-level";
|
|
422
|
+
}
|
|
423
|
+
if (schemaVersion < RESPONSE_MODEL_FOOTER_MIGRATION_VERSION) {
|
|
424
|
+
settings.features.responseModelFooter = settings.features.responseModel;
|
|
425
|
+
}
|
|
426
|
+
for (const entry of MENU_ENTRIES) {
|
|
427
|
+
if (entry.group === "decorations" && entry.cycleEnabledBy && entry.cycleDisabledValue !== undefined && !settings.decorations[entry.cycleEnabledBy]) {
|
|
428
|
+
setDecorationCycleValue(settings.decorations, entry.key, entry.cycleDisabledValue);
|
|
422
429
|
}
|
|
423
|
-
|
|
424
|
-
|
|
430
|
+
}
|
|
431
|
+
return settings;
|
|
425
432
|
}
|
|
426
433
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
throw err;
|
|
436
|
-
}
|
|
434
|
+
const settingsStore = createSettingsStore<DecoratorSettings>({
|
|
435
|
+
path: settingsPath,
|
|
436
|
+
codec: { sanitize: sanitizeSettings, version: { key: "schemaVersion", current: SETTINGS_SCHEMA_VERSION } },
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
/** A missing, unreadable or malformed file reads as the defaults. Never throws. */
|
|
440
|
+
export function loadSettings(): DecoratorSettings {
|
|
441
|
+
return settingsStore.read();
|
|
437
442
|
}
|
|
438
443
|
|
|
439
|
-
|
|
440
|
-
|
|
444
|
+
/** Atomic write with `schemaVersion` first. Returns the store result so callers can report a failure. */
|
|
445
|
+
export function saveSettings(settings: DecoratorSettings): SettingsWriteResult<DecoratorSettings> {
|
|
446
|
+
return settingsStore.write(settings);
|
|
441
447
|
}
|
package/src/setup-check.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
|
-
import { atomicWriteFile } from "
|
|
4
|
+
import { atomicWriteFile } from "@underactive/pi-topping-primitives/settings";
|
|
5
5
|
import { isPlainObject } from "./util.ts";
|
|
6
6
|
|
|
7
7
|
const setupCheckPath = (): string => join(getAgentDir(), "pi-topping", "setup-check.json");
|
package/src/setup-command.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { hyperlink } from "@earendil-works/pi-tui";
|
|
3
|
-
import { type MenuValue, showMenu } from "
|
|
3
|
+
import { type MenuSection, type MenuValue, showMenu } from "@underactive/pi-topping-primitives/menu";
|
|
4
4
|
import { type PiInstallResult, spawnPiInstall, spawnPiRemove } from "./pi-installer.ts";
|
|
5
5
|
import { setSetupCheckDisabled } from "./setup-check.ts";
|
|
6
6
|
import { detectToppings, type ToppingStatus } from "./toppings.ts";
|
|
@@ -132,11 +132,12 @@ export function registerSetupCommand(
|
|
|
132
132
|
const rows = new Map<string, SetupRow>();
|
|
133
133
|
for (const status of missing) rows.set(`install:${status.topping.pkg}`, { status, action: "install" });
|
|
134
134
|
for (const status of removable) rows.set(`remove:${status.topping.pkg}`, { status, action: "remove" });
|
|
135
|
-
const sections = [];
|
|
135
|
+
const sections: MenuSection[] = [];
|
|
136
136
|
if (missing.length > 0) {
|
|
137
137
|
sections.push({
|
|
138
138
|
title: "Missing Toppings",
|
|
139
139
|
items: missing.map(({ topping, onDisk }) => ({
|
|
140
|
+
kind: "toggle" as const,
|
|
140
141
|
id: `install:${topping.pkg}`,
|
|
141
142
|
label: `${topping.pkg}${onDisk ? " (on disk, not active)" : ""}`,
|
|
142
143
|
value: true,
|
|
@@ -147,6 +148,7 @@ export function registerSetupCommand(
|
|
|
147
148
|
sections.push({
|
|
148
149
|
title: "Installed Toppings",
|
|
149
150
|
items: removable.map(({ topping, active }) => ({
|
|
151
|
+
kind: "toggle" as const,
|
|
150
152
|
id: `remove:${topping.pkg}`,
|
|
151
153
|
label: `${topping.pkg}${active ? " (active)" : " (installed, not active)"}`,
|
|
152
154
|
value: false,
|
|
@@ -158,8 +160,11 @@ export function registerSetupCommand(
|
|
|
158
160
|
title: "Pi Topping: Setup",
|
|
159
161
|
sections,
|
|
160
162
|
hints: ["↑↓ move", "PgUp/PgDn page", "␣ toggle", "⏎ apply", "esc cancel"],
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
preview: {
|
|
164
|
+
title: "Description",
|
|
165
|
+
// The shared menu adds no margin; keep the one-space indent the local menu drew.
|
|
166
|
+
render: (values, { activeItemId }) => ({ lines: renderDescription(rows, activeItemId, values).map((line) => ` ${line}`) }),
|
|
167
|
+
},
|
|
163
168
|
});
|
|
164
169
|
if (!result.applied) {
|
|
165
170
|
ctx.ui.notify("Pi Topping setup cancelled.", "info");
|
package/src/util.ts
CHANGED
|
@@ -3,33 +3,15 @@ export function isPlainObject(value: unknown): value is Record<string, unknown>
|
|
|
3
3
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
/** Strip control and formatting characters from untrusted single-line text. */
|
|
7
|
-
export
|
|
8
|
-
return text.replace(/[\p{Cc}\p{Cf}]/gu, "");
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function sanitizedText(value: unknown): string {
|
|
12
|
-
return typeof value === "string" ? stripControlChars(value).trim() : "";
|
|
13
|
-
}
|
|
6
|
+
/** Strip control and formatting characters from untrusted single-line text; `sanitizedText` also trims and reads a non-string as empty. */
|
|
7
|
+
export { sanitizedText, stripControlChars } from "@underactive/pi-topping-primitives/text";
|
|
14
8
|
|
|
15
9
|
/**
|
|
16
10
|
* True when `version` is a release older than `minimum`, both read as `major.minor.patch`.
|
|
17
11
|
* A value that does not start with three numbers cannot be compared and counts as new enough;
|
|
18
12
|
* a pre-release suffix is ignored.
|
|
19
13
|
*/
|
|
20
|
-
export
|
|
21
|
-
const parse = (text: string): number[] | undefined => {
|
|
22
|
-
const match = /^(\d+)\.(\d+)\.(\d+)/.exec(text.trim());
|
|
23
|
-
return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
|
|
24
|
-
};
|
|
25
|
-
const actual = parse(version);
|
|
26
|
-
const floor = parse(minimum);
|
|
27
|
-
if (!actual || !floor) return false;
|
|
28
|
-
for (let i = 0; i < 3; i++) {
|
|
29
|
-
if (actual[i] !== floor[i]) return actual[i]! < floor[i]!;
|
|
30
|
-
}
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
14
|
+
export { hostBelow } from "@underactive/pi-topping-primitives/process";
|
|
33
15
|
|
|
34
16
|
/** True when a reported response model appears to be a decorated form of the selected model. */
|
|
35
17
|
export function modelsResemble(selectedModel?: string, responseModel?: string): boolean {
|
package/src/activity-meter.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { dimAttribute } from "./format.ts";
|
|
3
|
-
|
|
4
|
-
export const ActivityMeterLevel = {
|
|
5
|
-
IDLE: 0,
|
|
6
|
-
LOW: 1,
|
|
7
|
-
MEDIUM: 2,
|
|
8
|
-
HIGH: 3,
|
|
9
|
-
PEAK_1: 4,
|
|
10
|
-
PEAK_2: 5,
|
|
11
|
-
PEAK_3: 6,
|
|
12
|
-
FULL: 7,
|
|
13
|
-
} as const;
|
|
14
|
-
|
|
15
|
-
export type ActivityMeterLevel = (typeof ActivityMeterLevel)[keyof typeof ActivityMeterLevel];
|
|
16
|
-
|
|
17
|
-
const EMA_ALPHA = 0.4;
|
|
18
|
-
export const DEFAULT_METER_UPPER_BOUND_TPS = 80;
|
|
19
|
-
export const METER_UPPER_BOUND_TPS_MIN = 10;
|
|
20
|
-
export const METER_UPPER_BOUND_TPS_MAX = 1000;
|
|
21
|
-
const BRAILLE: Record<ActivityMeterLevel, string> = {
|
|
22
|
-
[ActivityMeterLevel.IDLE]: "⢀",
|
|
23
|
-
[ActivityMeterLevel.LOW]: "⣀",
|
|
24
|
-
[ActivityMeterLevel.MEDIUM]: "⣠",
|
|
25
|
-
[ActivityMeterLevel.HIGH]: "⣤",
|
|
26
|
-
[ActivityMeterLevel.PEAK_1]: "⣴",
|
|
27
|
-
[ActivityMeterLevel.PEAK_2]: "⣶",
|
|
28
|
-
[ActivityMeterLevel.PEAK_3]: "⣾",
|
|
29
|
-
[ActivityMeterLevel.FULL]: "⣿",
|
|
30
|
-
};
|
|
31
|
-
const WIDTH = 8;
|
|
32
|
-
const LEGACY_FULL_SCALE_TPS = 40;
|
|
33
|
-
// Levels below full retain the original 40 tps scale's proportions.
|
|
34
|
-
const RATE_THRESHOLDS = [0, 5, 10, 15, 22, 30] as const;
|
|
35
|
-
type CellColorizer = (level: ActivityMeterLevel, char: string) => string;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Convert an estimated output-token rate to a display level. Rates at or above `upperBoundTps`
|
|
39
|
-
* render as full; the lower levels scale in proportion to the bound.
|
|
40
|
-
*/
|
|
41
|
-
export function rateToLevel(tokensPerSecond: number, upperBoundTps: number = DEFAULT_METER_UPPER_BOUND_TPS): ActivityMeterLevel {
|
|
42
|
-
if (tokensPerSecond >= upperBoundTps) return ActivityMeterLevel.FULL;
|
|
43
|
-
for (let i = RATE_THRESHOLDS.length - 1; i >= 0; i--) {
|
|
44
|
-
if (tokensPerSecond * LEGACY_FULL_SCALE_TPS > RATE_THRESHOLDS[i]! * upperBoundTps) return (i + 1) as ActivityMeterLevel;
|
|
45
|
-
}
|
|
46
|
-
return ActivityMeterLevel.IDLE;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** EMA-smoothed rate tracker for a cumulative output-token estimate. */
|
|
50
|
-
export class TokRateTracker {
|
|
51
|
-
#lastTotal = 0;
|
|
52
|
-
#lastTime = 0;
|
|
53
|
-
#rate = 0;
|
|
54
|
-
#hasSample = false;
|
|
55
|
-
#pendingTokens = 0;
|
|
56
|
-
|
|
57
|
-
/** Latest EMA-smoothed output-token rate. */
|
|
58
|
-
get tokenRate(): number {
|
|
59
|
-
return this.#rate;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
sample(totalTokens: number, now: number): number {
|
|
63
|
-
if (!this.#hasSample) {
|
|
64
|
-
this.#lastTotal = totalTokens;
|
|
65
|
-
this.#hasSample = true;
|
|
66
|
-
this.#lastTime = now;
|
|
67
|
-
return this.#rate;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const elapsedSeconds = (now - this.#lastTime) / 1_000;
|
|
71
|
-
if (elapsedSeconds <= 0) {
|
|
72
|
-
this.#pendingTokens += Math.max(0, totalTokens - this.#lastTotal);
|
|
73
|
-
this.#lastTotal = totalTokens;
|
|
74
|
-
return this.#rate;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const totalDelta = this.#pendingTokens + Math.max(0, totalTokens - this.#lastTotal);
|
|
78
|
-
const instantRate = totalDelta / elapsedSeconds;
|
|
79
|
-
this.#rate = EMA_ALPHA * instantRate + (1 - EMA_ALPHA) * this.#rate;
|
|
80
|
-
this.#lastTotal = totalTokens;
|
|
81
|
-
this.#lastTime = now;
|
|
82
|
-
this.#pendingTokens = 0;
|
|
83
|
-
return this.#rate;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
reset(): void {
|
|
87
|
-
this.#lastTotal = 0;
|
|
88
|
-
this.#lastTime = 0;
|
|
89
|
-
this.#rate = 0;
|
|
90
|
-
this.#hasSample = false;
|
|
91
|
-
this.#pendingTokens = 0;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Eight-column scrolling activity meter, scrolling either left-to-right or right-to-left. */
|
|
96
|
-
export class ActivityMeter {
|
|
97
|
-
#levels: ActivityMeterLevel[] = Array<ActivityMeterLevel>(WIDTH).fill(0);
|
|
98
|
-
#direction: "ltr" | "rtl";
|
|
99
|
-
|
|
100
|
-
constructor(direction: "ltr" | "rtl" = "ltr") {
|
|
101
|
-
this.#direction = direction;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
setDirection(direction: "ltr" | "rtl"): void {
|
|
105
|
-
if (direction !== this.#direction) {
|
|
106
|
-
this.#direction = direction;
|
|
107
|
-
// Reverse the existing data so the visual flow instantly flips.
|
|
108
|
-
this.#levels.reverse();
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
push(level: ActivityMeterLevel): void {
|
|
113
|
-
if (this.#direction === "rtl") {
|
|
114
|
-
this.#levels.shift();
|
|
115
|
-
this.#levels.push(level);
|
|
116
|
-
} else {
|
|
117
|
-
this.#levels.pop();
|
|
118
|
-
this.#levels.unshift(level);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
render(colorize?: CellColorizer): string {
|
|
123
|
-
return this.#levels
|
|
124
|
-
.map((level) => {
|
|
125
|
-
const char = BRAILLE[level];
|
|
126
|
-
return colorize ? colorize(level, char) : char;
|
|
127
|
-
})
|
|
128
|
-
.join("");
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
reset(): void {
|
|
132
|
-
this.#levels.fill(0);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/** Colorize a meter cell (dim at IDLE, `colorize` otherwise). */
|
|
136
|
-
static colorizeCell(
|
|
137
|
-
level: ActivityMeterLevel,
|
|
138
|
-
char: string,
|
|
139
|
-
theme: { fg: (style: ThemeColor, s: string) => string },
|
|
140
|
-
colorize: (text: string) => string,
|
|
141
|
-
dimmed?: boolean,
|
|
142
|
-
): string {
|
|
143
|
-
if (level === ActivityMeterLevel.IDLE) return theme.fg("dim", char);
|
|
144
|
-
const colored = colorize(char);
|
|
145
|
-
return dimmed ? dimAttribute(colored) : colored;
|
|
146
|
-
}
|
|
147
|
-
}
|