@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/preview.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import {
|
|
3
|
-
import { ActivityMeter, METER_UPPER_BOUND_TPS_MAX, METER_UPPER_BOUND_TPS_MIN, rateToLevel } from "
|
|
4
|
-
import {
|
|
2
|
+
import type { MenuValue, PreviewSlot } from "@underactive/pi-topping-primitives/menu";
|
|
3
|
+
import { ActivityMeter, METER_UPPER_BOUND_TPS_MAX, METER_UPPER_BOUND_TPS_MIN, rateToLevel } from "@underactive/pi-topping-primitives/activity";
|
|
4
|
+
import { dimAttribute, renderMeter, thinkingLevelColorizer } from "@underactive/pi-topping-primitives/activity-tui";
|
|
5
|
+
import { DEFAULT_WORKING_WORD, ELAPSED_INTERVAL_MS, formatTokenRate, METER_INTERVAL_MS, SHIMMER_INTERVAL_MS, SPINNER_FRAME_MS } from "./format.ts";
|
|
5
6
|
import { buildLoaderMessage } from "./loader-message.ts";
|
|
6
7
|
import { getResponseModelColorizer } from "./nvidia-green.ts";
|
|
7
8
|
import { isOutputStyleId, OUTPUT_STYLES } from "./output-style.ts";
|
|
@@ -12,12 +13,24 @@ import { isWordPackEnabled, selectWorkingTextSelection, WORD_PACK_MENU_PREFIX, w
|
|
|
12
13
|
// Simulated load for the menu preview: a 2.4s cosine wave peaking at 46 tps for the meter,
|
|
13
14
|
// flat 28 tps for the token readouts.
|
|
14
15
|
const METER_PERIOD_MS = 2400, METER_PEAK_RATE = 46, TOKEN_RATE_PER_SEC = 28;
|
|
16
|
+
/** Width the preview assumes when rendered outside a menu (tests and probes). */
|
|
17
|
+
export const DEFAULT_PREVIEW_WIDTH = 76;
|
|
18
|
+
type Values = Record<string, MenuValue>;
|
|
19
|
+
type PreviewState = { elapsedMs: number; activeItemId: string | undefined; width: number };
|
|
20
|
+
export interface PreviewResult {
|
|
21
|
+
lines: string[];
|
|
22
|
+
nextRefreshInMs?: number;
|
|
23
|
+
}
|
|
24
|
+
/** The shared menu adds no margin; the preview indents every line by one space itself. */
|
|
25
|
+
function indented(result: PreviewResult): PreviewResult {
|
|
26
|
+
return { ...result, lines: result.lines.map(line => ` ${line}`) };
|
|
27
|
+
}
|
|
15
28
|
const PROMPT_IDS = new Set(MENU_ENTRIES.filter(entry => entry.section === "User Prompt").map(entry => entry.id));
|
|
16
29
|
const MARKER_IDS = new Set(MENU_ENTRIES.filter(entry => entry.section === "Completion Marker").map(entry => entry.id));
|
|
17
30
|
function meterRate(elapsedMs: number): number { return ((1 - Math.cos((2 * Math.PI * elapsedMs) / METER_PERIOD_MS)) / 2) * METER_PEAK_RATE; }
|
|
18
31
|
|
|
19
32
|
/** Stateful, per-menu preview renderer that follows the active settings section. */
|
|
20
|
-
export class PreviewRenderer {
|
|
33
|
+
export class PreviewRenderer implements PreviewSlot<Values> {
|
|
21
34
|
// Preserve constructor-time randomness so rendering never changes the preview phrase.
|
|
22
35
|
readonly #poolFraction = Math.random();
|
|
23
36
|
readonly #packs: readonly WordPack[];
|
|
@@ -30,7 +43,11 @@ export class PreviewRenderer {
|
|
|
30
43
|
this.#ctx = ctx;
|
|
31
44
|
this.#packs = packs;
|
|
32
45
|
}
|
|
33
|
-
render(values:
|
|
46
|
+
render(values: Values, state: PreviewState = { elapsedMs: 0, activeItemId: undefined, width: DEFAULT_PREVIEW_WIDTH }): PreviewResult {
|
|
47
|
+
return indented(this.renderContent(values, state.elapsedMs, state.activeItemId, Math.max(0, state.width - 1)));
|
|
48
|
+
}
|
|
49
|
+
/** The lines before the one-space indent; `width` is the budget those lines may fill. */
|
|
50
|
+
private renderContent(values: Values, elapsedMs: number, activeItemId: string | undefined, width: number): PreviewResult {
|
|
34
51
|
if (PROMPT_IDS.has(activeItemId ?? "")) return this.promptPreview(values, width);
|
|
35
52
|
if (MARKER_IDS.has(activeItemId ?? "")) return this.markerPreview(values, width);
|
|
36
53
|
if (activeItemId === "useNerdFont") return { lines: ["", `Icon preview: ${values.useNerdFont ? "" : "π"}`, ""] };
|
|
@@ -60,7 +77,7 @@ export class PreviewRenderer {
|
|
|
60
77
|
: "";
|
|
61
78
|
return { lines: ["", this.loaderPreview(values, elapsedMs), help], nextRefreshInMs };
|
|
62
79
|
}
|
|
63
|
-
private loaderPreview(values:
|
|
80
|
+
private loaderPreview(values: Values, elapsedMs: number): string {
|
|
64
81
|
this.#meter.setDirection(fromCycleDirection(values.meterDirection));
|
|
65
82
|
if (elapsedMs - this.#lastMeterUpdate >= METER_INTERVAL_MS) {
|
|
66
83
|
this.#meter.push(rateToLevel(meterRate(elapsedMs), fromCycleMeterUpperBound(values.meterUpperBoundTps)));
|
|
@@ -97,14 +114,14 @@ export class PreviewRenderer {
|
|
|
97
114
|
spinnerAtMs: values.animatedSpinner !== false ? elapsedMs : undefined,
|
|
98
115
|
word,
|
|
99
116
|
shimmerAtMs: elapsedMs,
|
|
100
|
-
renderMeter: (colorizer, dimmed) => this.#meter
|
|
117
|
+
renderMeter: (colorizer, dimmed) => renderMeter(this.#meter, theme, colorizer, dimmed),
|
|
101
118
|
elapsedMs,
|
|
102
119
|
outputTokens: Math.max(0, Math.floor(elapsedMs / 1000 * TOKEN_RATE_PER_SEC)),
|
|
103
120
|
tokenRateText: formatTokenRate(TOKEN_RATE_PER_SEC),
|
|
104
121
|
responseModel: features.responseModel ? "test-model" : undefined,
|
|
105
122
|
});
|
|
106
123
|
}
|
|
107
|
-
private packValues(values:
|
|
124
|
+
private packValues(values: Values): Record<string, boolean> {
|
|
108
125
|
const enabled: Record<string, boolean> = {};
|
|
109
126
|
for (const pack of this.#packs) {
|
|
110
127
|
const value = values[`${WORD_PACK_MENU_PREFIX}${pack.id}`];
|
|
@@ -112,14 +129,14 @@ export class PreviewRenderer {
|
|
|
112
129
|
}
|
|
113
130
|
return enabled;
|
|
114
131
|
}
|
|
115
|
-
private packPreview(id: string, values:
|
|
132
|
+
private packPreview(id: string, values: Values): PreviewResult {
|
|
116
133
|
const pack = this.#packs.find((candidate) => candidate.id === id);
|
|
117
134
|
if (!pack) return { lines: ["This word pack is unavailable."] };
|
|
118
135
|
const enabled = isWordPackEnabled(id, this.packValues(values));
|
|
119
136
|
const source = pack.bundled ? "Shipped example" : `Custom: ${wordPacksPath()}`;
|
|
120
137
|
return { lines: [`${pack.name}: ${enabled ? "enabled" : "disabled"}`, `${pack.words.length} phrases · ${source}`, ...(pack.description ? [pack.description] : [])] };
|
|
121
138
|
}
|
|
122
|
-
private promptPreview(values:
|
|
139
|
+
private promptPreview(values: Values, width: number): PreviewResult {
|
|
123
140
|
if (values.decorateUserPrompt !== true) return { lines: ["User prompt decoration is disabled."] };
|
|
124
141
|
|
|
125
142
|
const borderColor = values.borderColor;
|
|
@@ -141,7 +158,7 @@ export class PreviewRenderer {
|
|
|
141
158
|
? { lines }
|
|
142
159
|
: { lines, nextRefreshInMs: 1_000 - (timestamp % 1_000) };
|
|
143
160
|
}
|
|
144
|
-
private markerPreview(values:
|
|
161
|
+
private markerPreview(values: Values, width: number): PreviewResult {
|
|
145
162
|
if (values.doneMarker !== true) return { lines: ["Completion marker is disabled."] };
|
|
146
163
|
const theme = this.#ctx.ui.theme;
|
|
147
164
|
const icon = values.doneMarkerIcon === true ? theme.fg("text", values.useNerdFont === true ? "" : "π") : "";
|
|
@@ -153,7 +170,7 @@ export class PreviewRenderer {
|
|
|
153
170
|
let model = "";
|
|
154
171
|
if (values.doneMarkerModel === true) {
|
|
155
172
|
const color = isThinkingLevelColor(values.doneMarkerModelColor) ? values.doneMarkerModelColor : DEFAULT_SETTINGS.decorations.doneMarkerModelColor;
|
|
156
|
-
const modelColored =
|
|
173
|
+
const modelColored = thinkingLevelColorizer(theme, color, this.#ctx.thinkingLevel)("test-model");
|
|
157
174
|
model = values.doneMarkerModelDimmed === true ? dimAttribute(modelColored) : modelColored;
|
|
158
175
|
}
|
|
159
176
|
const content = buildCompletionMarkerContent(theme, icon, word, "2m 52s", details, model);
|
package/src/prompt-decorator.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import type { MessageRenderer, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
3
|
-
import {
|
|
1
|
+
import type { EntryRenderer, MessageRenderer, Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component } from "@earendil-works/pi-tui";
|
|
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";
|
|
10
|
+
export const PROMPT_BOX_ENTRY_TYPE = "pi-topping-prompt-entry";
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
single: { tl: "┌", tr: "┐", bl: "└", br: "┘", h: "─", v: "│" },
|
|
12
|
-
rounded: { tl: "╭", tr: "╮", bl: "╰", br: "╯", h: "─", v: "│" },
|
|
13
|
-
heavy: { tl: "┏", tr: "┓", bl: "┗", br: "┛", h: "━", v: "┃" },
|
|
14
|
-
};
|
|
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;
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
16
|
* False visibility options hide each corresponding part; present provider and
|
|
@@ -34,6 +33,11 @@ export interface PromptBoxDetails {
|
|
|
34
33
|
thinkingLevel?: ThinkingLevel;
|
|
35
34
|
}
|
|
36
35
|
|
|
36
|
+
export interface PromptBoxEntryData {
|
|
37
|
+
content: string;
|
|
38
|
+
details: PromptBoxDetails;
|
|
39
|
+
}
|
|
40
|
+
|
|
37
41
|
type PromptTheme = {
|
|
38
42
|
fg(color: string, text: string): string;
|
|
39
43
|
getThinkingBorderColor(level: ThinkingLevel): (text: string) => string;
|
|
@@ -73,12 +77,12 @@ export function buildCompletionMarkerLine(
|
|
|
73
77
|
if (borderStyle === "none") return truncateToWidth(content, safeWidth);
|
|
74
78
|
|
|
75
79
|
const g = BORDER_GLYPHS[borderStyle];
|
|
76
|
-
const border =
|
|
77
|
-
const prefix = `${g.
|
|
80
|
+
const border = thinkingLevelColorizer(theme, borderColor, thinkingLevel);
|
|
81
|
+
const prefix = `${g.bottomLeft}${g.horizontal.repeat(2)} `;
|
|
78
82
|
|
|
79
83
|
const suffix = markerStyle === "bookend"
|
|
80
|
-
? ` ${g.
|
|
81
|
-
: ` ${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(" ")}`;
|
|
82
86
|
const line = `${border(prefix)}${content}${border(suffix)}`;
|
|
83
87
|
return visibleWidth(line) > safeWidth ? truncateToWidth(line, safeWidth) : line;
|
|
84
88
|
}
|
|
@@ -97,7 +101,7 @@ export function buildPromptBoxLines(
|
|
|
97
101
|
const g = BORDER_GLYPHS[borderStyle];
|
|
98
102
|
const borderColor = isPromptBorderColor(options.borderColor) ? options.borderColor : DEFAULT_SETTINGS.decorations.borderColor;
|
|
99
103
|
const thinkingLevel = isThinkingLevel(options.thinkingLevel) ? options.thinkingLevel : undefined;
|
|
100
|
-
const border =
|
|
104
|
+
const border = thinkingLevelColorizer(theme, borderColor, thinkingLevel);
|
|
101
105
|
const iconColor = (text: string) => theme.fg("text", text);
|
|
102
106
|
const muted = (text: string) => theme.fg("dim", text);
|
|
103
107
|
const bodyText = (text: string) => theme.fg("customMessageText", text);
|
|
@@ -125,12 +129,12 @@ export function buildPromptBoxLines(
|
|
|
125
129
|
const labelText = combinedLabel
|
|
126
130
|
? truncateToWidth(combinedLabel, Math.max(0, innerWidth - 3)).replace(/\x1b\[0m/g, "")
|
|
127
131
|
: "";
|
|
128
|
-
const titleLength = icon ? visibleWidth(`${g.
|
|
132
|
+
const titleLength = icon ? visibleWidth(`${g.horizontal}${g.horizontal} ${icon} `) : 0;
|
|
129
133
|
const timeLength = time ? time.length + 3 : 0; // time + right-side " ═"
|
|
130
134
|
const topFill = Math.max(0, innerWidth - titleLength - timeLength);
|
|
131
|
-
const iconSeg = icon ? `${border(`${g.
|
|
132
|
-
const timeSeg = time ? `${border(" ")}${muted(time)}${border(` ${g.
|
|
133
|
-
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);
|
|
134
138
|
const lines = [visibleWidth(topLine) > width ? truncateToWidth(topLine, width) : topLine];
|
|
135
139
|
|
|
136
140
|
if (content) {
|
|
@@ -138,28 +142,27 @@ export function buildPromptBoxLines(
|
|
|
138
142
|
for (const rawLine of wrapTextWithAnsi(content, textWidth)) {
|
|
139
143
|
const displayLine = rawLine || " ";
|
|
140
144
|
const padded = `${displayLine}${" ".repeat(Math.max(0, textWidth - visibleWidth(displayLine)))}`;
|
|
141
|
-
const line = `${border(g.
|
|
145
|
+
const line = `${border(g.vertical)}${bodyText(` ${padded} `)}${border(g.vertical)}`;
|
|
142
146
|
lines.push(visibleWidth(line) > width ? truncateToWidth(line, width) : line);
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
const labelLength = labelText ? visibleWidth(labelText) + 3 : 0; // label + right-side " ═"
|
|
147
|
-
const labelSeg = labelText ? `${border(" ")}${muted(labelText)}${border(` ${g.
|
|
148
|
-
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)}`;
|
|
149
153
|
lines.push(visibleWidth(bottomLine) > width ? truncateToWidth(bottomLine, width) : bottomLine);
|
|
150
154
|
return lines;
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
|
|
154
|
-
const
|
|
155
|
-
const details = (message.details ?? {}) as PromptBoxDetails;
|
|
157
|
+
function createPromptBoxComponent(content: string, details: PromptBoxDetails, theme: Theme): Component {
|
|
158
|
+
const safeContent = content.replace(/(?![\n\t])[\p{Cc}\p{Cf}]/gu, "");
|
|
156
159
|
let cachedWidth: number | undefined;
|
|
157
160
|
let cachedLines: string[] | undefined;
|
|
158
161
|
return {
|
|
159
162
|
render(width: number): string[] {
|
|
160
163
|
if (cachedWidth !== width) {
|
|
161
164
|
cachedWidth = width;
|
|
162
|
-
cachedLines = buildPromptBoxLines(
|
|
165
|
+
cachedLines = buildPromptBoxLines(safeContent, details.submittedAt, width, theme, details);
|
|
163
166
|
}
|
|
164
167
|
return cachedLines!;
|
|
165
168
|
},
|
|
@@ -168,4 +171,16 @@ export const promptBoxRenderer: MessageRenderer<PromptBoxDetails> = (message, _o
|
|
|
168
171
|
cachedLines = undefined;
|
|
169
172
|
},
|
|
170
173
|
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export const promptBoxRenderer: MessageRenderer<PromptBoxDetails> = (message, _options, theme) => {
|
|
177
|
+
const content = typeof message.content === "string" ? message.content : "";
|
|
178
|
+
const details = (message.details ?? {}) as PromptBoxDetails;
|
|
179
|
+
return createPromptBoxComponent(content, details, theme);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const promptBoxEntryRenderer: EntryRenderer<PromptBoxEntryData> = (entry, _options, theme) => {
|
|
183
|
+
const data = entry.data;
|
|
184
|
+
if (!data || typeof data.content !== "string") return undefined;
|
|
185
|
+
return createPromptBoxComponent(data.content, data.details ?? {}, theme);
|
|
171
186
|
};
|